• 中文
  • EditableProTable - 可编辑表格

    可编辑表格 EditableProTable 与 ProTable 的功能基本相同,为了方便使用 EditableProTable 增加了一些预设,关掉了查询表单和操作栏,同时修改了 value 和 onChange 使其可以方便的继承到 antd 的 Form 中。

    代码演示

    编辑模式切换

    通过 Segmented 切换 editable.type,在单行编辑(single)和多行编辑(multiple)之间一键切换。

    editable.type 编辑模式:
    编辑模式切换
    任务名称状态描述创建时间操作
    暂无数据
    暂无数据
    表格数据
    []
    import type { ProColumns } from '@ant-design/pro-components';
    import {
      EditableProTable,
      ProCard,
      ProFormField,
    } from '@ant-design/pro-components';
    import { Segmented } from 'antd';
    import React, { useState } from 'react';
    
    import { createEditableRowId, DEMO_TASK_STATUS_ENUM } from '../mockData';
    
    type DataSourceType = {
      id: React.Key;
      title?: string;
      description?: string;
      status?: string;
      created_at?: number;
    };
    
    const defaultData: DataSourceType[] = [
      {
        id: 624748504,
        title: '优化首页加载速度',
        description: '首页白屏时间超过 3s,需优化资源加载',
        status: 'open',
        created_at: 1705286400000,
      },
      {
        id: 624691229,
        title: '修复登录超时问题',
        description: '高峰期登录请求超时,需排查连接池',
        status: 'closed',
        created_at: 1705200000000,
      },
      {
        id: 624674560,
        title: '新增数据导出功能',
        description: '支持导出 Excel 和 CSV 格式',
        status: 'processing',
        created_at: 1705113600000,
      },
    ];
    
    const Demo = () => {
      const [editableType, setEditableType] = useState<'single' | 'multiple'>(
        'multiple',
      );
      const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
      const [dataSource, setDataSource] = useState<readonly DataSourceType[]>([]);
    
      const columns: ProColumns<DataSourceType>[] = [
        {
          title: '任务名称',
          dataIndex: 'title',
          width: '30%',
        },
        {
          title: '状态',
          dataIndex: 'status',
          valueType: 'select',
          valueEnum: DEMO_TASK_STATUS_ENUM,
        },
        {
          title: '描述',
          dataIndex: 'description',
        },
        {
          title: '创建时间',
          dataIndex: 'created_at',
          valueType: 'date',
        },
        {
          title: '操作',
          valueType: 'option',
          width: 150,
          render: (text, record, _, action) => [
            <a key="editable" onClick={() => action?.startEditable?.(record.id)}>
              编辑
            </a>,
            <a
              key="delete"
              onClick={() =>
                setDataSource(dataSource.filter((item) => item.id !== record.id))
              }
            >
              删除
            </a>,
          ],
        },
      ];
    
      return (
        <div>
          <div style={{ marginBlockEnd: 16 }}>
            <span>editable.type 编辑模式:</span>
            <Segmented
              value={editableType}
              onChange={(v) => {
                setEditableType(v as any);
                setEditableRowKeys([]);
              }}
              options={[
                { label: '单行编辑 single', value: 'single' },
                { label: '多行编辑 multiple', value: 'multiple' },
              ]}
            />
          </div>
          <EditableProTable<DataSourceType>
            rowKey="id"
            headerTitle="编辑模式切换"
            columns={columns}
            request={async () => ({
              data: defaultData,
              total: defaultData.length,
              success: true,
            })}
            value={dataSource}
            onChange={setDataSource}
            recordCreatorProps={{
              position: 'bottom',
              record: () => ({ id: createEditableRowId() }),
              newRecordType: 'dataSource',
            }}
            editable={{
              type: editableType,
              editableKeys,
              onChange: setEditableRowKeys,
            }}
          />
          <ProCard title="表格数据" headerBordered collapsible defaultCollapsed>
            <ProFormField
              ignoreFormItem
              fieldProps={{ style: { width: '100%' } }}
              mode="read"
              valueType="jsonCode"
              text={JSON.stringify(dataSource)}
            />
          </ProCard>
        </div>
      );
    };
    
    export default () => (
      <div style={{ padding: 24 }}>
        <Demo />
      </div>
    );
    

    可编辑表格

    任务管理
    任务名称
    任务名称(只读)
    状态描述创建时间操作
    暂无数据
    暂无数据
    表格数据
    []
    import type { ProColumns } from '@ant-design/pro-components';
    import {
      EditableProTable,
      ProCard,
      ProFormField,
      ProFormRadio,
    } from '@ant-design/pro-components';
    import React, { useState } from 'react';
    
    import { createEditableRowId, DEMO_TASK_STATUS_ENUM } from '../../mockData';
    
    const waitTime = (time: number = 100) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(true);
        }, time);
      });
    };
    
    type DataSourceType = {
      id: React.Key;
      title?: string;
      readonly?: string;
      description?: string;
      status?: string;
      created_at?: number;
      update_at?: number;
      children?: DataSourceType[];
    };
    
    const defaultData: DataSourceType[] = [
      {
        id: 624748504,
        title: '优化首页加载速度',
        readonly: '优化首页加载速度',
        description: '首页白屏时间超过 3s,需优化资源加载和首屏渲染',
        status: 'open',
        created_at: 1705286400000,
        update_at: 1705372800000,
      },
      {
        id: 624691229,
        title: '修复登录超时问题',
        readonly: '修复登录超时问题',
        description: '用户反馈高峰期登录请求超时,需排查连接池配置',
        status: 'closed',
        created_at: 1705200000000,
        update_at: 1705286400000,
      },
    ];
    
    const Demo = () => {
      const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
      const [dataSource, setDataSource] = useState<readonly DataSourceType[]>([]);
      const [position, setPosition] = useState<'top' | 'bottom' | 'hidden'>(
        'bottom',
      );
    
      const columns: ProColumns<DataSourceType>[] = [
        {
          title: '任务名称',
          dataIndex: 'title',
          tooltip: '只读,使用form.getFieldValue获取不到值',
          formItemProps: (form, { rowIndex }) => {
            return {
              rules:
                rowIndex !== undefined && rowIndex > 1
                  ? [{ required: true, message: '此项为必填项' }]
                  : [],
            };
          },
          editable: (text, record, index) => {
            return index !== 0;
          },
          width: '15%',
        },
        {
          title: '任务名称(只读)',
          dataIndex: 'readonly',
          tooltip: '只读,使用form.getFieldValue可以获取到值',
          readonly: true,
          width: '15%',
        },
        {
          title: '状态',
          key: 'status',
          dataIndex: 'status',
          valueType: 'select',
          valueEnum: DEMO_TASK_STATUS_ENUM,
        },
        {
          title: '描述',
          dataIndex: 'description',
          fieldProps: (form, { rowKey, rowIndex }) => {
            if (form.getFieldValue([rowKey || '', 'title']) === '暂不处理') {
              return {
                disabled: true,
              };
            }
            if (rowIndex !== undefined && rowIndex > 9) {
              return {
                disabled: true,
              };
            }
            return {};
          },
        },
        {
          title: '创建时间',
          dataIndex: 'created_at',
          valueType: 'date',
        },
        {
          title: '操作',
          valueType: 'option',
          width: 200,
          render: (text, record, _, action) => [
            <a
              key="editable"
              onClick={() => {
                action?.startEditable?.(record.id);
              }}
            >
              编辑
            </a>,
            <a
              key="delete"
              onClick={() => {
                setDataSource(dataSource.filter((item) => item.id !== record.id));
              }}
            >
              删除
            </a>,
          ],
        },
      ];
    
      return (
        <>
          <EditableProTable<DataSourceType>
            rowKey="id"
            headerTitle="任务管理"
            maxLength={5}
            scroll={{
              x: 960,
            }}
            recordCreatorProps={
              position !== 'hidden'
                ? {
                    position: position as 'top',
                    record: () => ({ id: createEditableRowId() }),
                    newRecordType: 'dataSource',
                  }
                : false
            }
            loading={false}
            toolBarRender={() => [
              <ProFormRadio.Group
                key="render"
                fieldProps={{
                  value: position,
                  onChange: (e) => setPosition(e.target.value),
                }}
                options={[
                  {
                    label: '添加到顶部',
                    value: 'top',
                  },
                  {
                    label: '添加到底部',
                    value: 'bottom',
                  },
                  {
                    label: '隐藏',
                    value: 'hidden',
                  },
                ]}
              />,
            ]}
            columns={columns}
            request={async () => ({
              data: defaultData,
              total: 3,
              success: true,
            })}
            value={dataSource}
            onChange={(value) => {
              setDataSource((data) => {
                const newMap = new Map(value.map((item) => [item.id, item]));
                const merged = data.map((item) => {
                  return newMap.get(item.id) || item;
                });
                value.forEach((item) => {
                  if (!data.find((old) => old.id === item.id)) {
                    merged.push(item);
                  }
                });
                return merged;
              });
            }}
            editable={{
              type: 'multiple',
              editableKeys,
              onSave: async (_rowKey, _data, _row) => {
                await waitTime(2000);
              },
              onChange: setEditableRowKeys,
            }}
          />
          <ProCard title="表格数据" headerBordered collapsible defaultCollapsed>
            <ProFormField
              ignoreFormItem
              fieldProps={{
                style: {
                  width: '100%',
                },
              }}
              mode="read"
              valueType="jsonCode"
              text={JSON.stringify(dataSource)}
            />
          </ProCard>
        </>
      );
    };
    
    export default () => (
      <div style={{ padding: 24 }}>
        <Demo />
      </div>
    );
    

    单元格编辑

    任务管理(单元格编辑)
    任务名称状态描述
    优化首页加载速度待处理首页白屏时间超过 3s,需优化资源加载
    修复登录超时问题已完成用户反馈高峰期登录请求超时
    新增数据导出功能待处理支持将查询结果导出为 Excel 和 CSV
    重构权限管理模块已完成现有权限模型不支持细粒度控制
    接入第三方支付 SDK待处理对接微信支付和支付宝当面付
    import type { ProColumns } from '@ant-design/pro-components';
    import { CellEditorTable } from '@ant-design/pro-components';
    import React, { useState } from 'react';
    
    type DataSourceType = {
      id: React.Key;
      title?: string;
      description?: string;
      status?: string;
      created_at?: number;
      children?: DataSourceType[];
    };
    
    const taskNames = [
      '优化首页加载速度',
      '修复登录超时问题',
      '新增数据导出功能',
      '重构权限管理模块',
      '接入第三方支付 SDK',
    ];
    
    const taskDescs = [
      '首页白屏时间超过 3s,需优化资源加载',
      '用户反馈高峰期登录请求超时',
      '支持将查询结果导出为 Excel 和 CSV',
      '现有权限模型不支持细粒度控制',
      '对接微信支付和支付宝当面付',
    ];
    
    const defaultData: DataSourceType[] = taskNames.map((name, index) => ({
      id: (1705286400000 + index).toString(),
      title: name,
      description: taskDescs[index],
      status: index % 2 === 0 ? 'open' : 'closed',
      created_at: 1705286400000 - index * 86400000,
    }));
    
    const Demo = () => {
      const [dataSource, setDataSource] = useState<readonly DataSourceType[]>(
        () => defaultData,
      );
    
      const columns: ProColumns<DataSourceType>[] = [
        {
          title: '任务名称',
          dataIndex: 'title',
          width: '30%',
          formItemProps: {
            rules: [
              {
                required: true,
                whitespace: true,
                message: '此项是必填项',
              },
              {
                max: 30,
                whitespace: true,
                message: '最长为 30 位',
              },
              {
                min: 4,
                whitespace: true,
                message: '最小为 4 位',
              },
            ],
          },
        },
        {
          title: '状态',
          key: 'status',
          dataIndex: 'status',
          valueType: 'select',
          valueEnum: {
            all: { text: '全部', status: 'Default' },
            open: {
              text: '待处理',
              status: 'Error',
            },
            closed: {
              text: '已完成',
              status: 'Success',
            },
          },
        },
        {
          title: '描述',
          dataIndex: 'description',
        },
      ];
    
      return (
        <CellEditorTable<DataSourceType>
          headerTitle="任务管理(单元格编辑)"
          columns={columns}
          rowKey="id"
          scroll={{
            x: 960,
          }}
          value={dataSource}
          onChange={setDataSource}
          recordCreatorProps={{
            newRecordType: 'dataSource',
            record: () => ({
              id: Date.now(),
            }),
          }}
        />
      );
    };
    
    export default () => (
      <div style={{ padding: 24 }}>
        <Demo />
      </div>
    );
    
    姓名年龄地址操作
    18杭州西湖区
    李四22北京海淀区
    import type { ProColumns } from '@ant-design/pro-components';
    import { EditableProTable } from '@ant-design/pro-components';
    import React from 'react';
    
    type DataSourceType = {
      id: React.Key;
      name?: string;
      age?: number;
      address?: string;
    };
    
    const defaultData: DataSourceType[] = [
      { id: 1, name: '张三', age: 18, address: '杭州西湖区' },
      { id: 2, name: '李四', age: 22, address: '北京海淀区' },
    ];
    
    export default () => {
      const columns: ProColumns<DataSourceType>[] = [
        { title: '姓名', dataIndex: 'name', key: 'name' },
        { title: '年龄', dataIndex: 'age', key: 'age' },
        { title: '地址', dataIndex: 'address', key: 'address' },
        {
          title: '操作',
          valueType: 'option',
          key: 'option',
          render: () => [<a key="edit">编辑</a>],
        },
      ];
    
      return (
        <EditableProTable<DataSourceType>
          rowKey="id"
          columns={columns}
          value={defaultData}
          search={false}
          // cell 粒度复合键 `${rowKey}:${dataIndex}`:只激活第 1 行的 name 单元格
          editable={{ editableKeys: ['1:name'] }}
        />
      );
    };
    

    行编辑

    任务管理(整行编辑)
    任务名称状态描述
    优化首页加载速度待处理首页白屏时间超过 3s,需优化资源加载
    修复登录超时问题已完成用户反馈高峰期登录请求超时
    新增数据导出功能待处理支持将查询结果导出为 Excel 和 CSV
    重构权限管理模块已完成现有权限模型不支持细粒度控制
    接入第三方支付 SDK待处理对接微信支付和支付宝当面付
    import type { ProColumns } from '@ant-design/pro-components';
    import { RowEditorTable } from '@ant-design/pro-components';
    import React, { useState } from 'react';
    
    type DataSourceType = {
      id: React.Key;
      title?: string;
      description?: string;
      status?: string;
      created_at?: number;
      children?: DataSourceType[];
    };
    
    const taskNames = [
      '优化首页加载速度',
      '修复登录超时问题',
      '新增数据导出功能',
      '重构权限管理模块',
      '接入第三方支付 SDK',
    ];
    
    const taskDescs = [
      '首页白屏时间超过 3s,需优化资源加载',
      '用户反馈高峰期登录请求超时',
      '支持将查询结果导出为 Excel 和 CSV',
      '现有权限模型不支持细粒度控制',
      '对接微信支付和支付宝当面付',
    ];
    
    const defaultData: DataSourceType[] = taskNames.map((name, index) => ({
      id: (1705286400000 + index).toString(),
      title: name,
      description: taskDescs[index],
      status: index % 2 === 0 ? 'open' : 'closed',
      created_at: 1705286400000 - index * 86400000,
    }));
    
    const Demo = () => {
      const [dataSource, setDataSource] = useState<readonly DataSourceType[]>(
        () => defaultData,
      );
    
      const columns: ProColumns<DataSourceType>[] = [
        {
          title: '任务名称',
          dataIndex: 'title',
          width: '30%',
          formItemProps: {
            rules: [
              {
                required: true,
                whitespace: true,
                message: '此项是必填项',
              },
              {
                max: 30,
                whitespace: true,
                message: '最长为 30 位',
              },
              {
                min: 4,
                whitespace: true,
                message: '最小为 4 位',
              },
            ],
          },
        },
        {
          title: '状态',
          key: 'status',
          dataIndex: 'status',
          valueType: 'select',
          valueEnum: {
            all: { text: '全部', status: 'Default' },
            open: {
              text: '待处理',
              status: 'Error',
            },
            closed: {
              text: '已完成',
              status: 'Success',
            },
          },
        },
        {
          title: '描述',
          dataIndex: 'description',
        },
      ];
    
      return (
        <RowEditorTable<DataSourceType>
          headerTitle="任务管理(整行编辑)"
          columns={columns}
          rowKey="id"
          scroll={{
            x: 960,
          }}
          value={dataSource}
          onChange={setDataSource}
          recordCreatorProps={{
            newRecordType: 'dataSource',
            record: () => ({
              id: Date.now(),
            }),
          }}
        />
      );
    };
    
    export default () => (
      <div style={{ padding: 24 }}>
        <Demo />
      </div>
    );
    

    与 FormItem 配合

    可编辑表格
    任务名称状态描述活动时间操作
    优化首页加载速度未解决首页白屏时间超过 3s,需优化资源加载和首屏渲染2024-01-15
    修复登录超时问题已解决用户反馈高峰期登录请求超时,需排查连接池配置2024-01-14
    表格数据
    [
      {
        "id": "624748504",
        "title": "优化首页加载速度",
        "decs": "首页白屏时间超过 3s,需优化资源加载和首屏渲染",
        "state": "open",
        "created_at": 1705286400000,
        "update_at": 1705372800000
      },
      {
        "id": "624691229",
        "title": "修复登录超时问题",
        "decs": "用户反馈高峰期登录请求超时,需排查连接池配置",
        "state": "closed",
        "created_at": 1705200000000,
        "update_at": 1705286400000
      }
    ]
    import type {
      EditableFormInstance,
      ProColumns,
      ProFormInstance,
    } from '@ant-design/pro-components';
    import {
      EditableProTable,
      ProCard,
      ProForm,
      ProFormDependency,
      ProFormField,
      ProFormSegmented,
      ProFormSwitch,
    } from '@ant-design/pro-components';
    import { Button, message } from 'antd';
    import isEmpty from 'es-toolkit/compat/isEmpty';
    import React, { useRef, useState } from 'react';
    
    import { createEditableRowId } from '../../mockData';
    
    type DataSourceType = {
      id: React.Key;
      title?: string;
      decs?: string;
      state?: string;
      created_at?: number;
      update_at?: number;
      children?: DataSourceType[];
    };
    
    const defaultData: DataSourceType[] = [
      {
        id: '624748504',
        title: '优化首页加载速度',
        decs: '首页白屏时间超过 3s,需优化资源加载和首屏渲染',
        state: 'open',
        created_at: 1705286400000,
        update_at: 1705372800000,
      },
      {
        id: '624691229',
        title: '修复登录超时问题',
        decs: '用户反馈高峰期登录请求超时,需排查连接池配置',
        state: 'closed',
        created_at: 1705200000000,
        update_at: 1705286400000,
      },
    ];
    
    let i = 0;
    
    const Demo = () => {
      const [editableKeys, setEditableRowKeys] = useState<React.Key[]>(() => []);
      const [position, setPosition] = useState<'top' | 'bottom' | 'hidden'>(
        'bottom',
      );
      const [controlled, setControlled] = useState<boolean>(false);
      const formRef = useRef<ProFormInstance<any>>();
      const editorFormRef = useRef<EditableFormInstance<DataSourceType>>();
      const columns: ProColumns<DataSourceType>[] = [
        {
          title: '任务名称',
          dataIndex: 'title',
          formItemProps: (_form, _config) =>{
            return ({
              rules: [
                // 自定义 validator 放前面,空值时显式通过,避免干扰 required
                {
                  warningOnly: true,
                  validator: (_rule: any, value: any) => {
                    if (isEmpty(value)) {
                      return Promise.resolve();
                    }
                    if ('warningOnly' === value) {
                      throw new Error('Warning Only');
                    }
                    return Promise.resolve();
                  },
                },
                { required: true, message: '此项为必填项' },
              ],
            });
          },
          width: '30%',
        },
        {
          title: '状态',
          key: 'state',
          dataIndex: 'state',
          valueType: 'select',
          valueEnum: {
            all: { text: '全部', status: 'Default' },
            open: {
              text: '未解决',
              status: 'Error',
            },
            closed: {
              text: '已解决',
              status: 'Success',
            },
          },
        },
        {
          title: '描述',
          dataIndex: 'decs',
        },
        {
          title: '活动时间',
          dataIndex: 'created_at',
          valueType: 'date',
        },
        {
          title: '操作',
          valueType: 'option',
          width: 200,
          render: (text, record, _, action) => [
            <a
              key="editable"
              onClick={() => {
                action?.startEditable?.(record.id, record);
              }}
            >
              编辑
            </a>,
            <a
              key="delete"
              onClick={() => {
                const tableDataSource = formRef.current?.getFieldValue(
                  'table',
                ) as DataSourceType[];
                formRef.current?.setFieldsValue({
                  table: tableDataSource.filter((item) => item.id !== record.id),
                });
              }}
            >
              删除
            </a>,
          ],
        },
      ];
    
      return (
        <ProForm<{
          table: DataSourceType[];
        }>
          formRef={formRef}
          initialValues={{
            table: defaultData,
          }}
          validateTrigger="onBlur"
        >
          <EditableProTable<DataSourceType>
            rowKey="id"
            scroll={{
              x: 960,
            }}
            editableFormRef={editorFormRef}
            headerTitle="可编辑表格"
            maxLength={5}
            name="table"
            controlled={controlled}
            recordCreatorProps={
              position !== 'hidden'
                ? {
                    position: position as 'top',
                    record: () => ({ id: createEditableRowId() }),
                  }
                : false
            }
            toolBarRender={() => [
              <ProFormSwitch
                key="render"
                fieldProps={{
                  style: {
                    marginBlockEnd: 0,
                  },
                  checked: controlled,
                  onChange: (value) => {
                    setControlled(value);
                  },
                }}
                checkedChildren="数据更新通知 Form"
                unCheckedChildren="保存后通知 Form"
                noStyle
              />,
              <ProFormSegmented
                key="render"
                fieldProps={{
                  style: {
                    marginBlockEnd: 0,
                  },
                  value: position,
                  onChange: (value) => {
                    setPosition(value as 'top');
                  },
                }}
                noStyle
                request={async () => [
                  {
                    label: '添加到顶部',
                    value: 'top',
                  },
                  {
                    label: '添加到底部',
                    value: 'bottom',
                  },
                  {
                    label: '隐藏',
                    value: 'hidden',
                  },
                ]}
              />,
              <Button
                key="rows"
                onClick={() => {
                  const rows = editorFormRef.current?.getRowsData?.();
                  message.info(JSON.stringify(rows ?? []));
                }}
              >
                获取 table 的数据
              </Button>,
            ]}
            columns={columns}
            editable={{
              type: 'multiple',
              editableKeys,
              onChange: setEditableRowKeys,
              actionRender: (row, config, defaultDom) => {
                return [
                  defaultDom.save,
                  defaultDom.delete,
                  defaultDom.cancel,
                  <a
                    key="set"
                    onClick={() => {
                      i++;
                      editorFormRef.current?.setRowData?.(config.index!, {
                        title: '动态设置的title' + i,
                      });
                    }}
                  >
                    动态设置此项
                  </a>,
                ];
              },
            }}
          />
          <ProForm.Item>
            <ProCard title="表格数据" headerBordered collapsible defaultCollapsed>
              <ProFormDependency name={['table']}>
                {({ table }) => {
                  return (
                    <ProFormField
                      ignoreFormItem
                      fieldProps={{
                        style: {
                          width: '100%',
                        },
                      }}
                      mode="read"
                      valueType="jsonCode"
                      text={JSON.stringify(table)}
                    />
                  );
                }}
              </ProFormDependency>
            </ProCard>
          </ProForm.Item>
        </ProForm>
      );
    };
    
    export default () => (
      <div style={{ padding: 24 }}>
        <Demo />
      </div>
    );
    

    与编辑表格外的内容联动

    总分:80
    题数:40
    关联题库题型题数计分方式分值操作
    前端基础知识多选题10continuous20
    React 框架进阶单选题10continuous20
    TypeScript 类型系统判断题10continuous20
    算法与数据结构填空题10continuous20
    import type {
      ActionType,
      EditableFormInstance,
      ProColumns,
      ProFormInstance,
    } from '@ant-design/pro-components';
    import {
      EditableProTable,
      ProCard,
      ProForm,
      ProFormDependency,
      ProFormDigit,
    } from '@ant-design/pro-components';
    import React, { useRef, useState } from 'react';
    
    type DataSourceType = {
      id: React.Key;
      associate?: string;
      questionsNum?: number;
      type?: string;
      fraction?: number;
      scoringMethod?: string;
    };
    
    const defaultData: DataSourceType[] = [
      {
        id: 624748504,
        associate: '前端基础知识',
        questionsNum: 10,
        type: 'multiple',
        scoringMethod: 'continuous',
        fraction: 20,
      },
      {
        id: 624691229,
        associate: 'React 框架进阶',
        questionsNum: 10,
        scoringMethod: 'continuous',
        type: 'radio',
        fraction: 20,
      },
      {
        id: 624748503,
        associate: 'TypeScript 类型系统',
        questionsNum: 10,
        type: 'judge',
        scoringMethod: 'continuous',
        fraction: 20,
      },
      {
        id: 624691220,
        associate: '算法与数据结构',
        questionsNum: 10,
        scoringMethod: 'continuous',
        type: 'vacant',
        fraction: 20,
      },
    ];
    
    const Demo = () => {
      const [editableKeys, setEditableRowKeys] = useState<React.Key[]>(() => []);
      const formRef = useRef<ProFormInstance<any>>();
      const actionRef = useRef<ActionType>();
      const editableFormRef = useRef<EditableFormInstance>();
      const columns: ProColumns<DataSourceType>[] = [
        {
          title: '关联题库',
          dataIndex: 'associate',
          valueType: 'text',
          ellipsis: true,
        },
        {
          title: '题型',
          key: 'type',
          dataIndex: 'type',
          valueType: 'select',
          valueEnum: {
            multiple: { text: '多选题', status: 'Default' },
            radio: { text: '单选题', status: 'Warning' },
            vacant: {
              text: '填空题',
              status: 'Error',
            },
            judge: {
              text: '判断题',
              status: 'Success',
            },
          },
        },
        {
          title: '题数',
          dataIndex: 'questionsNum',
          valueType: 'digit',
        },
        {
          title: '计分方式',
          dataIndex: 'scoringMethod',
          valueType: 'select',
          request: async () => [
            {
              value: 'discrete',
              label: '离散型',
            },
            {
              value: 'continuous',
              label: '连续型',
            },
          ],
          fieldProps: (_, { rowIndex }) => {
            return {
              onSelect: () => {
                // 每次选中重置参数
                if (rowIndex !== undefined) {
                  editableFormRef.current?.setRowData?.(rowIndex, { fraction: [] });
                }
              },
            };
          },
        },
        {
          title: '分值',
          width: 150,
          dataIndex: 'fraction',
          valueType: (record) => {
            const scoringMethod = record?.scoringMethod;
            if (scoringMethod === 'discrete') return 'select';
            return 'digit';
          },
          fieldProps: {
            mode: 'multiple',
          },
          request: async () =>
            ['A', 'B', 'D', 'E', 'F'].map((item, index) => ({
              label: item,
              value: index,
            })),
        },
        {
          title: '操作',
          valueType: 'option',
          render: (_, row) => [
            <a
              key="delete"
              onClick={() => {
                const tableDataSource = formRef.current?.getFieldValue(
                  'table',
                ) as DataSourceType[];
                formRef.current?.setFieldsValue({
                  table: tableDataSource.filter((item) => item.id !== row?.id),
                });
              }}
            >
              移除
            </a>,
            <a
              key="edit"
              onClick={() => {
                actionRef.current?.startEditable(row.id);
              }}
            >
              编辑
            </a>,
          ],
        },
      ];
    
      return (
        <ProCard>
          <div
            style={{
              maxWidth: 800,
              margin: 'auto',
            }}
          >
            <ProForm<{
              table: DataSourceType[];
            }>
              formRef={formRef}
              initialValues={{
                table: defaultData,
              }}
            >
              <ProFormDependency name={['table']}>
                {({ table }) => {
                  const info = (table as DataSourceType[]).reduce(
                    (pre, item) => {
                      return {
                        totalScore:
                          pre.totalScore +
                          parseInt((item?.fraction || 0).toString(), 10),
                        questions:
                          pre.questions +
                          parseInt((item?.questionsNum || 0).toString(), 10),
                      };
                    },
                    { totalScore: 0, questions: 0 },
                  );
                  return (
                    <div
                      style={{
                        display: 'flex',
                        alignItems: 'center',
                        gap: 16,
                        paddingBlockEnd: 16,
                      }}
                    >
                      <div style={{ flex: 1 }}>总分:{info.totalScore}</div>
                      <div style={{ flex: 1 }}>题数:{info.questions}</div>
                      <div style={{ flex: 2 }}>
                        <ProFormDigit label="及格分" />
                      </div>
                      <div style={{ flex: 2 }}>
                        <ProFormDigit label="考试时间(分钟)" />
                      </div>
                    </div>
                  );
                }}
              </ProFormDependency>
              <EditableProTable<DataSourceType>
                rowKey="id"
                scroll={{
                  x: true,
                }}
                editableFormRef={editableFormRef}
                controlled
                actionRef={actionRef}
                formItemProps={{
                  label: '题库编辑',
                  rules: [
                    {
                      validator: async (_, value) => {
                        if (value.length < 1) {
                          throw new Error('请至少添加一个题库');
                        }
    
                        if (value.length > 5) {
                          throw new Error('最多可以设置五个题库');
                        }
                      },
                    },
                  ],
                }}
                maxLength={10}
                name="table"
                columns={columns}
                recordCreatorProps={{
                  record: (index) => {
                    return { id: index + 1 };
                  },
                }}
                editable={{
                  type: 'multiple',
                  editableKeys,
                  onChange: setEditableRowKeys,
                }}
              />
            </ProForm>
          </div>
        </ProCard>
      );
    };
    
    export default () => (
      <div style={{ padding: 24 }}>
        <Demo />
      </div>
    );
    

    有子列的表格增加

    可编辑表格
    任务名称状态描述活动时间操作
    优化首页加载速度未解决首页白屏时间超过 3s,需优化资源加载和首屏渲染2024-01-15
    拆分首屏关键资源已解决将非关键 JS/CSS 延迟加载2024-01-14
    修复登录超时问题已解决用户反馈高峰期登录请求超时,需排查连接池配置2024-01-14
    表格数据
    [
      {
        "id": 624748504,
        "title": "优化首页加载速度",
        "decs": "首页白屏时间超过 3s,需优化资源加载和首屏渲染",
        "state": "open",
        "created_at": 1705286400000,
        "update_at": 1705372800000,
        "children": [
          {
            "id": 6246912293,
            "title": "拆分首屏关键资源",
            "decs": "将非关键 JS/CSS 延迟加载",
            "state": "closed",
            "created_at": 1705200000000,
            "update_at": 1705286400000
          }
        ]
      },
      {
        "id": 624691229,
        "title": "修复登录超时问题",
        "decs": "用户反馈高峰期登录请求超时,需排查连接池配置",
        "state": "closed",
        "created_at": 1705200000000,
        "update_at": 1705286400000
      }
    ]
    import type { ProColumns } from '@ant-design/pro-components';
    import {
      EditableProTable,
      ProCard,
      ProFormField,
      useRefFunction,
    } from '@ant-design/pro-components';
    import React, { useState } from 'react';
    
    import { createEditableRowId } from '../../mockData';
    
    const waitTime = (time: number = 100) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(true);
        }, time);
      });
    };
    
    type DataSourceType = {
      id?: React.Key;
      title?: string;
      decs?: string;
      state?: string;
      created_at?: number;
      update_at?: number;
      children?: DataSourceType[];
    };
    
    const defaultData: DataSourceType[] = [
      {
        id: 624748504,
        title: '优化首页加载速度',
        decs: '首页白屏时间超过 3s,需优化资源加载和首屏渲染',
        state: 'open',
        created_at: 1705286400000,
        update_at: 1705372800000,
        children: [
          {
            id: 6246912293,
            title: '拆分首屏关键资源',
            decs: '将非关键 JS/CSS 延迟加载',
            state: 'closed',
            created_at: 1705200000000,
            update_at: 1705286400000,
          },
        ],
      },
      {
        id: 624691229,
        title: '修复登录超时问题',
        decs: '用户反馈高峰期登录请求超时,需排查连接池配置',
        state: 'closed',
        created_at: 1705200000000,
        update_at: 1705286400000,
      },
    ];
    
    const loopDataSourceFilter = (
      data: readonly DataSourceType[],
      id: React.Key | undefined,
    ): DataSourceType[] => {
      return data
        .map((item) => {
          if (item.id !== id) {
            if (item.children) {
              const newChildren = loopDataSourceFilter(item.children, id);
              return {
                ...item,
                children: newChildren.length > 0 ? newChildren : undefined,
              };
            }
            return item;
          }
          return null;
        })
        .filter(Boolean) as DataSourceType[];
    };
    
    const Demo = () => {
      const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
      const [dataSource, setDataSource] = useState<readonly DataSourceType[]>(
        () => defaultData,
      );
    
      const removeRow = useRefFunction((record: DataSourceType) => {
        setDataSource(loopDataSourceFilter(dataSource, record.id));
      });
      const columns: ProColumns<DataSourceType>[] = [
        {
          title: '任务名称',
          dataIndex: 'title',
          formItemProps: (form, { rowIndex }) => {
            return {
              rules:
                rowIndex !== undefined && rowIndex > 2
                  ? [{ required: true, message: '此项为必填项' }]
                  : [],
            };
          },
          width: '30%',
        },
        {
          title: '状态',
          key: 'state',
          dataIndex: 'state',
          valueType: 'select',
          valueEnum: {
            all: { text: '全部', status: 'Default' },
            open: {
              text: '未解决',
              status: 'Error',
            },
            closed: {
              text: '已解决',
              status: 'Success',
            },
          },
        },
        {
          title: '描述',
          dataIndex: 'decs',
          fieldProps: (form, { rowKey, rowIndex }) => {
            if (form.getFieldValue([rowKey || '', 'title']) === '不好玩') {
              return {
                disabled: true,
              };
            }
            if (rowIndex !== undefined && rowIndex > 9) {
              return {
                disabled: true,
              };
            }
            return {};
          },
        },
        {
          title: '活动时间',
          dataIndex: 'created_at',
          valueType: 'date',
        },
        {
          title: '操作',
          valueType: 'option',
          width: 200,
          render: (text, record) => [
            <a
              key="delete"
              onClick={() => {
                removeRow(record);
              }}
            >
              删除
            </a>,
          ],
        },
      ];
    
      return (
        <>
          <EditableProTable<DataSourceType>
            expandable={{
              // 使用 request 请求数据时无效
              defaultExpandAllRows: true,
            }}
            scroll={{
              x: 960,
            }}
            rowKey="id"
            headerTitle="可编辑表格"
            maxLength={5}
            recordCreatorProps={{
              position: 'bottom',
              newRecordType: 'dataSource',
              parentKey: () => 624748504,
              record: () => ({ id: createEditableRowId() }),
            }}
            columns={columns}
            value={dataSource}
            onChange={setDataSource}
            editable={{
              type: 'multiple',
              editableKeys,
              onSave: async (_rowKey, _data, _row) => {
                await waitTime(2000);
              },
              onChange: setEditableRowKeys,
            }}
          />
          <ProCard title="表格数据" headerBordered collapsible defaultCollapsed>
            <ProFormField
              ignoreFormItem
              fieldProps={{
                style: {
                  width: '100%',
                },
              }}
              mode="read"
              valueType="jsonCode"
              text={JSON.stringify(dataSource)}
            />
          </ProCard>
        </>
      );
    };
    
    export default () => (
      <div style={{ padding: 24 }}>
        <Demo />
      </div>
    );
    

    自定义可编辑表格

    自定义可编辑表格
    任务名称状态标签操作
    暂无数据
    暂无数据
    表格数据
    []
    import { PlusOutlined } from '@ant-design/icons';
    import type { ActionType, ProColumns } from '@ant-design/pro-components';
    import {
      EditableProTable,
      ProCard,
      ProFormField,
    } from '@ant-design/pro-components';
    import type { InputRef } from 'antd';
    import { Button, Form, Input, Space, Tag } from 'antd';
    import React, { useRef, useState } from 'react';
    
    import { createEditableRowId } from '../../mockData';
    
    const waitTime = (time: number = 100) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(true);
        }, time);
      });
    };
    
    
    const TagList = React.forwardRef<
      HTMLDivElement,
      {
        value?: {
          key: string;
          label: string;
        }[];
        onChange?: (
          value: {
            key: string;
            label: string;
          }[],
        ) => void;
      }
    >(({ value, onChange }, ref) => {
      const inputRef = useRef<InputRef | null>(null);
      const [newTags, setNewTags] = useState<
        {
          key: string;
          label: string;
        }[]
      >([]);
      const [inputValue, setInputValue] = useState<string>('');
    
      const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setInputValue(e.target.value);
      };
    
      const handleInputConfirm = () => {
        let tempsTags = [...(value || [])];
        if (
          inputValue &&
          tempsTags.filter((tag) => tag.label === inputValue).length === 0
        ) {
          tempsTags = [
            ...tempsTags,
            { key: `new-${tempsTags.length}`, label: inputValue },
          ];
        }
        onChange?.(tempsTags);
        setNewTags([]);
        setInputValue('');
      };
    
      return (
        <Space ref={ref}>
          {(value || []).concat(newTags).map((item) => (
            <Tag key={item.key}>{item.label}</Tag>
          ))}
          <Input
            ref={inputRef}
            type="text"
            size="small"
            style={{ width: 78 }}
            value={inputValue}
            onChange={handleInputChange}
            onBlur={handleInputConfirm}
            onPressEnter={handleInputConfirm}
          />
        </Space>
      );
    });
    TagList.displayName = 'TagList';
    
    type DataSourceType = {
      id: React.Key;
      title?: string;
      labels?: {
        key: string;
        label: string;
      }[];
      status?: string;
      created_at?: number;
      children?: DataSourceType[];
    };
    
    const defaultData: DataSourceType[] = [
      {
        id: 624748504,
        title: '优化首页加载速度',
        labels: [{ key: 'perf', label: '性能优化' }],
        status: 'open',
        created_at: 1705286400000,
      },
      {
        id: 624691229,
        title: '修复登录超时问题',
        labels: [{ key: 'bug', label: '缺陷修复' }],
        status: 'closed',
        created_at: 1705200000000,
      },
    ];
    
    const columns: ProColumns<DataSourceType>[] = [
      {
        title: '任务名称',
        dataIndex: 'title',
        formItemProps: {
          rules: [
            {
              required: true,
              message: '此项为必填项',
            },
          ],
        },
        width: '30%',
      },
      {
        title: '状态',
        key: 'status',
        dataIndex: 'status',
        valueType: 'select',
        valueEnum: {
          all: { text: '全部', status: 'Default' },
          open: {
            text: '待处理',
            status: 'Error',
          },
          closed: {
            text: '已完成',
            status: 'Success',
          },
        },
      },
      {
        title: '标签',
        dataIndex: 'labels',
        width: '20%',
        formItemProps: {
          rules: [
            {
              required: true,
              message: '此项为必填项',
            },
          ],
        },
        formItemRender: (_, { isEditable }) => {
          return isEditable ? <TagList /> : <Input />;
        },
        render: (_, row) =>
          row?.labels?.map((item) => <Tag key={item.key}>{item.label}</Tag>),
      },
      {
        title: '操作',
        valueType: 'option',
        width: 250,
        render: (text, record, _, action) => [
          <a
            key="editable"
            onClick={() => {
              action?.startEditable?.(record.id);
            }}
          >
            编辑
          </a>,
          <EditableProTable.RecordCreator
            key="copy"
            record={{
              ...record,
              id: createEditableRowId(),
            }}
          >
            <a>复制此项到末尾</a>
          </EditableProTable.RecordCreator>,
        ],
      },
    ];
    
    const Demo = () => {
      const actionRef = useRef<ActionType>();
      const [editableKeys, setEditableRowKeys] = useState<React.Key[]>([]);
      const [dataSource, setDataSource] = useState<readonly DataSourceType[]>([]);
      const [form] = Form.useForm();
      return (
        <>
          <Space>
            <Button
              type="primary"
              onClick={() => {
                actionRef.current?.addEditRecord?.({
                  id: createEditableRowId(),
                  title: '新建任务',
                });
              }}
              icon={<PlusOutlined />}
            >
              新建一行
            </Button>
            <Button
              key="rest"
              onClick={() => {
                form.resetFields();
              }}
            >
              重置表单
            </Button>
          </Space>
    
          <EditableProTable<DataSourceType>
            rowKey="id"
            scroll={{
              x: 960,
            }}
            actionRef={actionRef}
            headerTitle="自定义可编辑表格"
            maxLength={5}
            recordCreatorProps={false}
            columns={columns}
            request={async () => ({
              data: defaultData,
              total: 3,
              success: true,
            })}
            value={dataSource}
            onChange={setDataSource}
            editable={{
              form,
              editableKeys,
              onSave: async () => {
                await waitTime(2000);
              },
              onChange: setEditableRowKeys,
              actionRender: (row, config, dom) => [dom.save, dom.cancel],
            }}
          />
          <ProCard title="表格数据" headerBordered collapsible defaultCollapsed>
            <ProFormField
              ignoreFormItem
              fieldProps={{
                style: {
                  width: '100%',
                },
              }}
              mode="read"
              valueType="jsonCode"
              text={JSON.stringify(dataSource)}
            />
          </ProCard>
        </>
      );
    };
    
    export default () => (
      <div style={{ padding: 24 }}>
        <Demo />
      </div>
    );
    

    实时保存的编辑表格

    实时编辑任务列表
    任务名称状态描述操作
    待处理
    进行中
    已完成
    待处理
    进行中
    已完成
    待处理
    进行中
    已完成
    待处理
    进行中
    已完成
    待处理
    进行中
    已完成
    待处理
    进行中
    已完成
    待处理
    进行中
    表格数据
    [
      {
        "id": "1705286400000",
        "title": "优化首页加载速度",
        "description": "首页白屏时间超过 3s",
        "status": "open",
        "created_at": 1705286400000
      },
      {
        "id": "1705286400001",
        "title": "修复登录超时问题",
        "description": "高峰期登录请求超时",
        "status": "processing",
        "created_at": 1705200000000
      },
      {
        "id": "1705286400002",
        "title": "新增数据导出功能",
        "description": "导出 Excel 和 CSV",
        "status": "closed",
        "created_at": 1705113600000
      },
      {
        "id": "1705286400003",
        "title": "重构权限管理模块",
        "description": "细粒度权限控制",
        "status": "open",
        "created_at": 1705027200000
      },
      {
        "id": "1705286400004",
        "title": "接入第三方支付 SDK",
        "description": "对接微信和支付宝",
        "status": "processing",
        "created_at": 1704940800000
      },
      {
        "id": "1705286400005",
        "title": "升级 React 18",
        "description": "升级到最新 LTS 版本",
        "status": "closed",
        "created_at": 1704854400000
      },
      {
        "id": "1705286400006",
        "title": "优化搜索算法",
        "description": "ES 查询响应慢",
        "status": "open",
        "created_at": 1704768000000
      },
      {
        "id": "1705286400007",
        "title": "增加单元测试覆盖",
        "description": "核心模块覆盖率 > 90%",
        "status": "processing",
        "created_at": 1704681600000
      },
      {
        "id": "1705286400008",
        "title": "实现深色模式",
        "description": "支持系统主题切换",
        "status": "closed",
        "created_at": 1704595200000
      },
      {
        "id": "1705286400009",
        "title": "接入埋点 SDK",
        "description": "用户行为数据采集",
        "status": "open",
        "created_at": 1704508800000
      },
      {
        "id": "1705286400010",
        "title": "优化图片懒加载",
        "description": "减少首屏图片请求",
        "status": "processing",
        "created_at": 1704422400000
      },
      {
        "id": "1705286400011",
        "title": "重构表格组件",
        "description": "提升大数据量渲染性能",
        "status": "closed",
        "created_at": 1704336000000
      },
      {
        "id": "1705286400012",
        "title": "添加国际化支持",
        "description": "支持中英日三语",
        "status": "open",
        "created_at": 1704249600000
      },
      {
        "id": "1705286400013",
        "title": "修复移动端适配",
        "description": "iOS Safari 布局异常",
        "status": "processing",
        "created_at": 1704163200000
      },
      {
        "id": "1705286400014",
        "title": "集成 CI/CD 流程",
        "description": "自动化构建和部署",
        "status": "closed",
        "created_at": 1704076800000
      },
      {
        "id": "1705286400015",
        "title": "优化打包体积",
        "description": "减少 vendor chunk 体积",
        "status": "open",
        "created_at": 1703990400000
      },
      {
        "id": "1705286400016",
        "title": "实现 WebSocket 通信",
        "description": "实时消息推送",
        "status": "processing",
        "created_at": 1703904000000
      },
      {
        "id": "1705286400017",
        "title": "添加操作审计日志",
        "description": "记录关键操作日志",
        "status": "closed",
        "created_at": 1703817600000
      },
      {
        "id": "1705286400018",
        "title": "优化缓存策略",
        "description": "减少 API 冗余请求",
        "status": "open",
        "created_at": 1703731200000
      },
      {
        "id": "1705286400019",
        "title": "重构路由模块",
        "description": "支持嵌套路由和权限路由",
        "status": "processing",
        "created_at": 1703644800000
      }
    ]
    import type { ProColumns } from '@ant-design/pro-components';
    import {
      EditableProTable,
      ProCard,
      ProFormField,
    } from '@ant-design/pro-components';
    import { Button } from 'antd';
    import React, { useState } from 'react';
    
    type DataSourceType = {
      id: React.Key;
      title?: string;
      description?: string;
      status?: string;
      created_at?: number;
      children?: DataSourceType[];
    };
    
    const taskNames = [
      '优化首页加载速度',
      '修复登录超时问题',
      '新增数据导出功能',
      '重构权限管理模块',
      '接入第三方支付 SDK',
      '升级 React 18',
      '优化搜索算法',
      '增加单元测试覆盖',
      '实现深色模式',
      '接入埋点 SDK',
      '优化图片懒加载',
      '重构表格组件',
      '添加国际化支持',
      '修复移动端适配',
      '集成 CI/CD 流程',
      '优化打包体积',
      '实现 WebSocket 通信',
      '添加操作审计日志',
      '优化缓存策略',
      '重构路由模块',
    ];
    
    const taskDescs = [
      '首页白屏时间超过 3s',
      '高峰期登录请求超时',
      '导出 Excel 和 CSV',
      '细粒度权限控制',
      '对接微信和支付宝',
      '升级到最新 LTS 版本',
      'ES 查询响应慢',
      '核心模块覆盖率 > 90%',
      '支持系统主题切换',
      '用户行为数据采集',
      '减少首屏图片请求',
      '提升大数据量渲染性能',
      '支持中英日三语',
      'iOS Safari 布局异常',
      '自动化构建和部署',
      '减少 vendor chunk 体积',
      '实时消息推送',
      '记录关键操作日志',
      '减少 API 冗余请求',
      '支持嵌套路由和权限路由',
    ];
    
    const defaultData: DataSourceType[] = taskNames.map((name, index) => ({
      id: (1705286400000 + index).toString(),
      title: name,
      description: taskDescs[index],
      status: index % 3 === 0 ? 'open' : index % 3 === 1 ? 'processing' : 'closed',
      created_at: 1705286400000 - index * 86400000,
    }));
    
    const Demo = () => {
      const [editableKeys, setEditableRowKeys] = useState<React.Key[]>(() =>
        defaultData.map((item) => item.id),
      );
      const [dataSource, setDataSource] = useState<readonly DataSourceType[]>(
        () => defaultData,
      );
    
      const columns: ProColumns<DataSourceType>[] = [
        {
          title: '任务名称',
          dataIndex: 'title',
          width: '30%',
          formItemProps: {
            rules: [
              {
                required: true,
                whitespace: true,
                message: '此项是必填项',
              },
              {
                max: 30,
                whitespace: true,
                message: '最长为 30 位',
              },
              {
                min: 4,
                whitespace: true,
                message: '最小为 4 位',
              },
            ],
          },
        },
        {
          title: '状态',
          key: 'status',
          dataIndex: 'status',
          valueType: 'select',
          valueEnum: {
            all: { text: '全部', status: 'Default' },
            open: {
              text: '待处理',
              status: 'Error',
            },
            processing: {
              text: '进行中',
              status: 'Processing',
            },
            closed: {
              text: '已完成',
              status: 'Success',
            },
          },
        },
        {
          title: '描述',
          dataIndex: 'description',
        },
        {
          title: '操作',
          valueType: 'option',
          width: 250,
          render: () => {
            return null;
          },
        },
      ];
    
      return (
        <>
          <EditableProTable<DataSourceType>
            headerTitle="实时编辑任务列表"
            columns={columns}
            rowKey="id"
            scroll={{
              x: 960,
            }}
            value={dataSource}
            onChange={setDataSource}
            recordCreatorProps={{
              newRecordType: 'dataSource',
              record: () => ({
                id: Date.now(),
              }),
            }}
            toolBarRender={() => {
              return [
                <Button
                  type="primary"
                  key="save"
                  onClick={() => {
                    console.log(dataSource);
                  }}
                >
                  保存数据
                </Button>,
              ];
            }}
            editable={{
              type: 'multiple',
              editableKeys,
              actionRender: (row, config, defaultDoms) => {
                return [defaultDoms.delete];
              },
              onValuesChange: (record, recordList) => {
                setDataSource(recordList);
              },
              onChange: setEditableRowKeys,
            }}
          />
          <ProCard title="表格数据" headerBordered collapsible defaultCollapsed>
            <ProFormField
              ignoreFormItem
              fieldProps={{
                style: {
                  width: '100%',
                },
              }}
              mode="read"
              valueType="jsonCode"
              text={JSON.stringify(dataSource)}
            />
          </ProCard>
        </>
      );
    };
    
    export default () => (
      <div style={{ padding: 24 }}>
        <Demo />
      </div>
    );
    

    复杂的编辑表格

    字段名称字段类型字段别名补充说明
    暂无数据
    暂无数据
    表格数据
    [
      {
        "name": "company_total",
        "value": {
          "schemaName": "namespace_e0d8926c7cd51a4ce3ca3675ee370478",
          "tableName": "company_total"
        },
        "columnsInfo": [
          {
            "name": "auto_id_by_system",
            "dataType": "bigint",
            "description": "系统自动生成-自增主键",
            "supplementComment": "系统自动生成-自增主键"
          },
          {
            "name": "project_name",
            "dataType": "text",
            "description": "project; 保险项目名称,包括赔款支出、保险金额、总资产、责任险、农业保险、原保险保费收入、健康险、意外险等。",
            "supplementComment": "project; 保险项目名称,包括赔款支出、保险金额、总资产、责任险、农业保险、原保险保费收入、健康险、意外险等。"
          },
          {
            "name": "amount",
            "dataType": "numeric(24,5)",
            "description": "amount; 数值金额,代表各种保险项目的金额。",
            "supplementComment": "amount; 数值金额,代表各种保险项目的金额。"
          }
        ],
        "description": "7月财产险公司经营情况表"
      },
      {
        "name": "part_compay_info",
        "value": {
          "schemaName": "namespace_e0d8926c7cd51a4ce3ca3675ee370478",
          "tableName": "part_compay_info"
        },
        "columnsInfo": [
          {
            "name": "auto_id_by_system",
            "dataType": "bigint",
            "description": "系统自动生成-自增主键",
            "supplementComment": "系统自动生成-自增主键"
          },
          {
            "name": "mct_one_id",
            "dataType": "text",
            "description": "mct_one_id; 唯一标识符",
            "supplementComment": "mct_one_id; 唯一标识符"
          },
          {
            "name": "list_type",
            "dataType": "text",
            "description": "list_type; 上市板块类型",
            "supplementComment": "list_type; 上市板块类型"
          },
          {
            "name": "report_year",
            "dataType": "numeric(24,5)",
            "description": "report_year; 报告年份",
            "supplementComment": "report_year; 报告年份"
          },
          {
            "name": "org_name",
            "dataType": "text",
            "description": "org_name; 公司名称",
            "supplementComment": "org_name; 公司名称"
          },
          {
            "name": "total_assets",
            "dataType": "numeric(24,5)",
            "description": "total_assets; 总资产",
            "supplementComment": "total_assets; 总资产"
          },
          {
            "name": "total_liability",
            "dataType": "numeric(24,5)",
            "description": "total_liability; 总负债",
            "supplementComment": "total_liability; 总负债"
          },
          {
            "name": "equity",
            "dataType": "numeric(24,5)",
            "description": "equity; 股东权益",
            "supplementComment": "equity; 股东权益"
          },
          {
            "name": "total_profit",
            "dataType": "numeric(24,5)",
            "description": "total_profit; 总利润",
            "supplementComment": "total_profit; 总利润"
          },
          {
            "name": "net_profit",
            "dataType": "numeric(24,5)",
            "description": "net_profit; 净利润",
            "supplementComment": "net_profit; 净利润"
          },
          {
            "name": "tax",
            "dataType": "numeric(24,5)",
            "description": "tax; 税额",
            "supplementComment": "tax; 税额"
          }
        ],
        "description": "企业年报"
      }
    ]
    import {
      ProCard,
      ProForm,
      ProFormDependency,
      ProFormField,
      ProTable,
    } from '@ant-design/pro-components';
    import { FormInstance, Input, Tabs } from 'antd';
    import { useRef } from 'react';
    
    const defaultData = [
      {
        name: 'company_total',
        value: {
          schemaName: 'namespace_e0d8926c7cd51a4ce3ca3675ee370478',
          tableName: 'company_total',
        },
        columnsInfo: [
          {
            name: 'auto_id_by_system',
            dataType: 'bigint',
            description: '系统自动生成-自增主键',
            supplementComment: '系统自动生成-自增主键',
          },
          {
            name: 'project_name',
            dataType: 'text',
            description:
              'project; 保险项目名称,包括赔款支出、保险金额、总资产、责任险、农业保险、原保险保费收入、健康险、意外险等。',
            supplementComment:
              'project; 保险项目名称,包括赔款支出、保险金额、总资产、责任险、农业保险、原保险保费收入、健康险、意外险等。',
          },
          {
            name: 'amount',
            dataType: 'numeric(24,5)',
            description: 'amount; 数值金额,代表各种保险项目的金额。',
            supplementComment: 'amount; 数值金额,代表各种保险项目的金额。',
          },
        ],
        description: '7月财产险公司经营情况表',
      },
      {
        name: 'part_compay_info',
        value: {
          schemaName: 'namespace_e0d8926c7cd51a4ce3ca3675ee370478',
          tableName: 'part_compay_info',
        },
        columnsInfo: [
          {
            name: 'auto_id_by_system',
            dataType: 'bigint',
            description: '系统自动生成-自增主键',
            supplementComment: '系统自动生成-自增主键',
          },
          {
            name: 'mct_one_id',
            dataType: 'text',
            description: 'mct_one_id; 唯一标识符',
            supplementComment: 'mct_one_id; 唯一标识符',
          },
          {
            name: 'list_type',
            dataType: 'text',
            description: 'list_type; 上市板块类型',
            supplementComment: 'list_type; 上市板块类型',
          },
          {
            name: 'report_year',
            dataType: 'numeric(24,5)',
            description: 'report_year; 报告年份',
            supplementComment: 'report_year; 报告年份',
          },
          {
            name: 'org_name',
            dataType: 'text',
            description: 'org_name; 公司名称',
            supplementComment: 'org_name; 公司名称',
          },
          {
            name: 'total_assets',
            dataType: 'numeric(24,5)',
            description: 'total_assets; 总资产',
            supplementComment: 'total_assets; 总资产',
          },
          {
            name: 'total_liability',
            dataType: 'numeric(24,5)',
            description: 'total_liability; 总负债',
            supplementComment: 'total_liability; 总负债',
          },
          {
            name: 'equity',
            dataType: 'numeric(24,5)',
            description: 'equity; 股东权益',
            supplementComment: 'equity; 股东权益',
          },
          {
            name: 'total_profit',
            dataType: 'numeric(24,5)',
            description: 'total_profit; 总利润',
            supplementComment: 'total_profit; 总利润',
          },
          {
            name: 'net_profit',
            dataType: 'numeric(24,5)',
            description: 'net_profit; 净利润',
            supplementComment: 'net_profit; 净利润',
          },
          {
            name: 'tax',
            dataType: 'numeric(24,5)',
            description: 'tax; 税额',
            supplementComment: 'tax; 税额',
          },
        ],
        description: '企业年报',
      },
    ];
    
    const Demo = () => {
      const formRef = useRef<FormInstance>();
      return (
        <ProForm
          formRef={formRef}
          onValuesChange={(_, _e) => {
    
          }}
          initialValues={{ columnsInfo: defaultData, table: defaultData }}
        >
          <ProFormDependency name={['columnsInfo']}>
            {({ columnsInfo }) => {
              const list = (columnsInfo || []) as any[];
              return (
                <Tabs
                  style={{
                    padding: '0 8px',
                    maxWidth: '100%',
                  }}
                  items={list.map((item: any, index) => {
                    return {
                      key: item.name || '',
                      tab: (
                        <div
                          style={{
                            padding: 4,
                          }}
                        >
                          {item.name}
                        </div>
                      ),
                      label: (
                        <div
                          style={{
                            padding: 4,
                          }}
                        >
                          {item.name}
                        </div>
                      ),
                      children: (
                        <div
                          style={{
                            display: 'flex',
                            maxWidth: '1024px',
                          }}
                        >
                          <div
                            style={{
                              flex: 1,
                              overflow: 'auto',
                            }}
                          >
                            <ProTable
                              scroll={{ x: 'max-content' }}
                              size="small"
                              pagination={false}
                              search={false}
                              editable={{
                                editableKeys: item?.columnsInfo?.map(
                                  (row: any) => row.name || '',
                                ),
                              }}
                              rowKey="name"
                              name={['table', index, 'columnsInfo']}
                              columns={[
                                {
                                  title: '字段名称',
                                  dataIndex: 'name',
                                  editable: false,
                                },
                                {
                                  title: '字段类型',
                                  dataIndex: 'dataType',
                                  editable: false,
                                },
                                {
                                  title: '字段别名',
                                  dataIndex: 'description',
                                  editable: false,
                                  width: 140,
                                  render: (text, record) => {
                                    return (
                                      <div
                                        style={{
                                          WebkitLineClamp: 2,
                                          maxHeight: 40,
                                          lineHeight: '20px',
                                          textOverflow: 'ellipsis',
                                          overflow: 'hidden',
                                          display: '-webkit-box',
                                          WebkitBoxOrient: 'vertical',
                                          wordBreak: 'break-all',
                                        }}
                                      >
                                        {record.description
                                          ? record.description
                                          : '-'}
                                      </div>
                                    );
                                  },
                                },
                                {
                                  title: '补充说明',
                                  dataIndex: 'supplementComment',
                                  formItemRender: () => {
                                    return (
                                      <Input.TextArea
                                        placeholder="请输入补充说明"
                                        maxLength={200}
                                      />
                                    );
                                  },
                                },
                              ]}
                              toolBarRender={false}
                              request={async () => {
                                return {
                                  data: item.columnsInfo,
                                };
                              }}
                            />
                          </div>
                        </div>
                      ),
                    };
                  })}
                />
              );
            }}
          </ProFormDependency>
    
          <ProCard title="表格数据" headerBordered collapsible defaultCollapsed>
            <ProFormDependency name={['table']}>
              {({ table }) => {
                return (
                  <ProFormField
                    ignoreFormItem
                    fieldProps={{
                      style: {
                        width: '100%',
                      },
                    }}
                    mode="read"
                    valueType="jsonCode"
                    text={JSON.stringify(table, null, 2)}
                  />
                );
              }}
            </ProFormDependency>
          </ProCard>
        </ProForm>
      );
    };
    
    export default () => (
      <div style={{ padding: 24 }}>
        <Demo />
      </div>
    );
    

    EditableProTable

    属性描述类型默认值
    value同 dataSource,传入一个数组,是 table 渲染的元数据T[]undefined
    onChangedataSource 修改时触发,删除和修改都会触发,如果设置了 value,Table 会成为一个受控组件。(value:T[])=>voidundefined
    recordCreatorProps新建一行数据的相关配置RecordCreatorProps & ButtonProps-
    maxLength最大的行数,到达最大行数新建按钮会自动消失number-
    editable可编辑表格的相关配置TableRowEditable-
    controlled是否受控,如果受控每次编辑都会触发 onChange,并且会修改 dataSourcebooleanfalse
    editableFormReftable 所有的 form,带了一些表格特有的操作React.Ref<EditableFormInstance<T>>undefined

    别的 API 与 ProTable 相同。

    Editable 新建行

    新增一行的时候要保证 recordCreatorProps.record key 唯一,不然会导致编辑出错。

    <EditableTable
      rowKey="id"
      recordCreatorProps={{
        position: position as 'top',
        // 每次新增的时候需要Key
        record: () => ({ id: getNewId() }),
      }}
    />

    EditableFormInstance 表格列表单操作

    相比于 ProForm 的表单,可编辑表格增加了以下的三个方法。

      /**
       * 获取一行数据的
       * @param rowIndex
       * @returns T | undefined
       *
       * @example getRowData(1)  可以传入第几行的数据
       * @example getRowData("id")  也可以传入 rowKey,根据你列的唯一key 来获得。
       */
      getRowData?: (rowIndex: string | number) => T | undefined;
      /**
       * 获取整个 table 的数据
       * @returns T[] | undefined
       */
      getRowsData?: () => T[] | undefined;
      /**
       * 设置一行的数据,会将数据进行简单的 merge
       *
       * {title:"old", decs:"old",id:"old"} -> set {title:"new"} -> {title:"new", decs:"old",id:"old"}
       *
       * @description 只会做最第一层对象的 merge 哦。
       * {title:"old", decs:{name:"old",key:"old"},id:"old"} -> set {decs:{name:"new"}} -> {title:"old", decs:{name:"new"},id:"old"} -> set {decs:{name:"old"}}
       *
       * @param rowIndex
       * @param data
       * @returns void
       *
       * 根据行号设置
       * @example setRowData(1, { title:"new" })  可以传入修改第几行
       *
       * 根据行 id 设置
       * @example setRowData("id", { title:"new" })  也可以传入 rowKey,根据你列的唯一 key 来设置。
       *
       * 清空原有数据
       * @example setRowData(1, { title:undefined })
       *
       */
      setRowData?: (rowIndex: string | number, data: Partial<T>) => void;

    TableRowEditable

    属性描述类型默认值
    type可编辑表格的类型,单行编辑或者多行编辑single | multiple-
    form可编辑表格的 form 实例,使用 Form.useForm 生成后使用FormInstance-
    formProps可以配置 form 的属性,但是不支持 onFinishFormProps-
    editableKeys正在编辑的行,受控属性。 默认 key 会使用 rowKey 的配置,如果没有配置会使用 index,建议使用 rowKey。支持 cell 粒度复合键 `${rowKey}:${dataIndex}`(如 ['1:name'] 只激活该单元格),行级 key 与 cell 键可混用;cell 编辑时 option 列不渲染保存/取消按钮Key[]-
    onChange行数据被修改的时候触发(editableKeys: Key[], editableRows: T[]) => void-
    onValuesChange行数据被修改的时候触发(record: T, dataSource: T[]) => void-
    onSave保存一行的时候触发(key: Key, row: T,originRow:T,newLine?:newLineConfig) => Promise<any>-
    saveText保存一行的文字React.ReactNode保存
    onDelete删除一行的时候触发(key: Key, row: T) => Promise<any>-
    deleteText删除一行的文字React.ReactNode删除
    onCancel取消编辑一行时触发(key: Key, row: T,originRow:T,newLine?:newLineConfig) => Promise<any>-
    cancelText取消编辑一行的文字React.ReactNode取消
    actionRender自定义编辑模式的操作栏(row: T, config: ActionRenderConfig<T>) => ReactNode[]-
    deletePopconfirmMessage删除时弹出的确认框提示消息ReactNode删除此项?
    onlyOneLineEditorAlertMessage只能编辑一行的的提示ReactNode只能同时编辑一行
    onlyAddOneLineAlertMessage只能同时新增一行的提示ReactNode只能新增一行
    tableNameTable 上设置的 name,用于拼接 name 来获取数据NamePath-
    getRealIndex解决分页带来的 FormItem namePath 使用错误的 index 作为路径(issue #7790)(record: T) => number-
    dateFormatter与 ProTable dateFormatter 一致;合并到 dataSource 时把行内的 dayjs 转为 string 或 number'string' | 'number' | false | ((value, valueType) => string | number)-

    RecordCreatorProps

    为了使用,我们预设了一个新建的功能,大多数情况下已经可以满足大部分新建的需求,但是很多时候需求总是千奇百怪。我们也准备了 recordCreatorProps 来控制生成按钮。与 Pro 系列组件的 API 相同,recordCreatorProps={false}就可以关掉按钮,同时使用 actionRef.current?.addEditRecord(row) 来控制新建行。

    recordCreatorProps 也支持自定义一些样式,position='top'|'bottom' 可以配置增加在表格头还是表格尾部。record 可以配置新增行的默认数据。以下是一个列举

    recordCreatorProps = {
      // 要增加到哪个节点下,一般用于多重嵌套表格
      parentKey: React.Key,
      // 顶部添加还是末尾添加
      position: 'bottom',
      // 新增一行的方式,默认是缓存,取消后就会消失
      // 如果设置为 dataSource 会触发 onchange,取消后也不会消失,只能删除
      newRecordType: 'dataSource',
      // 不写 key ,会使用 index 当行 id
      record: {},
      // 设置按钮文案
      creatorButtonText: '新增一行',
      // 按钮的样式设置,可以设置按钮是否显示
      // 这样可以做最大行限制和最小行限制之类的功能
      style: {
        display: 'none',
      },
      // https://ant.design/components/button-cn/#API
      ...antButtonProps,
    };

    formItemRender 自定义编辑组件

    虽然我们很希望默认的 valueType 可以满足所有的需求,但是现实往往不尽如人意。所以我们也提供了 formItemRender 来自定义编辑输入组件。

    formItemRender 可以理解为在 Form.Item 下面加入一个元素, 伪代码实现是下面这样的:

    const dom = formItemRender();
    
    <Form.Item>{dom}</Form.Item>;

    所以与 Form.Item 相同,我们认为 formItemRender 返回的组件都是拥有的 value 和 onChange 的,我们接下来将看到用 formItemRender 将一个简单的 TagList 组件放入可编辑表格中。

    没有 value 将会无法注入值,没有 onChange 会无法修改行数据

    首先我们定义一个 TagList 组件。

    const TagList: React.FC<{
      value?: {
        key: string;
        label: string;
      }[];
      onChange?: (
        value: {
          key: string;
          label: string;
        }[],
      ) => void;
    }> = ({ value, onChange }) => {
      const ref = useRef<Input | null>(null);
      const [newTags, setNewTags] = useState<
        {
          key: string;
          label: string;
        }[]
      >([]);
      const [inputValue, setInputValue] = useState<string>('');
    
      const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        setInputValue(e.target.value);
      };
    
      const handleInputConfirm = () => {
        let tempsTags = [...(value || [])];
        if (
          inputValue &&
          tempsTags.filter((tag) => tag.label === inputValue).length === 0
        ) {
          tempsTags = [
            ...tempsTags,
            { key: `new-${tempsTags.length}`, label: inputValue },
          ];
        }
        onChange?.(tempsTags);
        setNewTags([]);
        setInputValue('');
      };
    
      return (
        <Space>
          {(value || []).concat(newTags).map((item) => (
            <Tag key={item.key}>{item.label}</Tag>
          ))}
          <Input
            ref={ref}
            type="text"
            size="small"
            style={{ width: 78 }}
            value={inputValue}
            onChange={handleInputChange}
            onBlur={handleInputConfirm}
            onPressEnter={handleInputConfirm}
          />
        </Space>
      );
    };

    在列中我们可以这样配置它。

     {
        title: '标签',
        dataIndex: 'labels',
        width: '40%',
        formItemRender: () => <TagList />,
        render: (_, row) => row?.labels?.map((item) => <Tag key={item.key}>{item.label}</Tag>),
      },

    转化成的编辑表格效果如下 :

    image.png

    value 和 onChange 会自动注入,我们不需要显式的注入。数据绑定也是由编辑表格自己注入的,我们在 onSave 中可以拿到处理完成的数据。虽然我们可以行内的写很复杂的逻辑甚至网路请求,但是我们推荐使用拆分组件,这样不仅性能更好,逻辑也可以拆分的很简单。

    formItemRender 同时也用来生成查询表单,如果我们需要区分这两种情况,可以使用 formItemRender: (_, { isEditable }) => (isEditable ? <TagList /> : <Input /> ) 这样的方式来进行分别渲染。

    actionRender 自定义操作栏

    可编辑表格默认提供了三大金刚, 保存,删除 和 取消,如果我们要实现复制一行,或者需求只需要的 保存和取消,不需要删除按钮就需要自定义了。可编辑表格提供了 API 来进行自定义,以下会直接展示代码:

    复制一行到底部

    render: (text, record, _, action) => [
      <a
        key="editable"
        onClick={() => {
          action?.startEditable?.(record.id);
        }}
      >
        编辑
      </a>,
      <EditableProTable.RecordCreator
        record={{
          ...record,
          id: (Math.random() * 1000000).toFixed(0),
        }}
      >
        <a>复制此项到末尾</a>
      </EditableProTable.RecordCreator>,
    ];

    自定义操作栏

    const editable = {
      // defaultDom = {save,cancel,delete} 可以酌情添加和使用
      actionRender: (row, config, defaultDom) => [
        defaultDom.save,
        defaultDom.cancel,
      ],
    };