Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/antd-plus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"ahooks": "^3.7.8",
"classnames": "^2.3.2",
"json5": "^2.2.3",
"lodash-es": "^4.17.21",
"lunar-javascript": "^1.6.7",
"moment": "^2.29.4",
"prefix-classnames": "^0.0.7",
Expand Down
171 changes: 171 additions & 0 deletions packages/antd-plus/src/editable-table/EditableTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { useControllableValue, useMemoizedFn } from 'ahooks';
import type { TablePaginationConfig, TableProps, FormItemProps } from 'antd';
import { Form, Input, Table } from 'antd';
import type { AnyObject } from 'antd/es/table/Table';
import type { NamePath } from 'antd/lib/form/interface';
import { cloneDeep, isFunction, isNull, isUndefined } from 'lodash-es';
import React, { useImperativeHandle, useMemo, useState } from 'react';
import type { InternalNamePath } from 'antd/es/form/interface';
import type { EditableColumnExtraRenderParams, EditableColumnType, EditableColumnsType, EditableTableActionType } from './types';

export interface EditableTableProps<RecordType extends AnyObject> extends Omit<TableProps<RecordType>, 'columns' | 'dataSource' | 'onChange'> {

/** 表单字段名称 */
name: NamePath;

/** 表格的 name 路径,用于嵌套多层 Form.List 时,在 render 时可以获取到表格当前行完整的 name 路径 */
tableNamePath?: InternalNamePath;

/** 是否只读 */
readonly?: boolean;

/** 列配置 */
columns?: EditableColumnsType<RecordType>;

/** 外部控制 EditableTable 编辑行的 ref */
actionRef?: React.MutableRefObject<EditableTableActionType<RecordType> | undefined>;

/** 受控的 rowKeys */
editableRowKeys?: (string | number)[];

/** onTableChange 代替 antd Table 的 onChange 进行使用 */
onTableChange?: TableProps<RecordType>['onChange'];

/** 等同于 dataSource */
value?: RecordType[];

/** onChange 返回当前 List 值 */
onChange?: (value?: RecordType[]) => void;

/** 默认渲染的 Form.Item 组件,内部默认使用 Input */
defaultFormItem?: React.ReactNode;
}

/**
* 判断给定的name参数是否为NamePath类型,如果不是undefined或null则返回true,否则返回false。
* @param name - 可选参数,NamePath类型的变量或null
* @returns boolean
*/
const isValidNamePath = (name?: NamePath | null): name is NamePath => !(isUndefined(name) || isNull(name));

const getNamePath = (name: NamePath) => (Array.isArray(name) ? name : [name]);

const InternalEditableTable = <RecordType extends AnyObject = AnyObject>(props: EditableTableProps<RecordType>) => {
const {
name,
tableNamePath = [],
columns,
actionRef,
defaultFormItem = <Input placeholder="请输入" />,
readonly,
rowKey: _rowKey,
editableRowKeys,
pagination = false,
onChange,
onTableChange,
...otherProps
} = props;

const [value, setValue] = useControllableValue<RecordType[] | undefined>(props);
const form = Form.useFormInstance();
const [_this] = useState<{ currentPagination: TablePaginationConfig }>({
currentPagination: !pagination ? { current: 1, pageSize: 10 } : pagination,
});

useImperativeHandle(actionRef, () => ({
addEditRecord: (record, insertIndex) => {
if (!readonly) {
const newValue = cloneDeep(value ?? []);
newValue.splice(insertIndex ?? newValue.length, 0, record ?? Object.assign({}));
setValue(newValue);
}
},
removeEditRecord: (rowIndex) => {
if (!readonly) {
setValue(value?.filter((_, i) => i !== rowIndex));
}
},
}));

// 读取当前 rowKey
const getRowKey = useMemoizedFn((record: RecordType, index?: number) => {
if (isFunction(_rowKey)) {
return _rowKey(record, index);
}

return (record as any)?.[_rowKey ?? 'key'];
});

const getNameIndex = useMemoizedFn((index: number) => {
if (!pagination) return index;
const { current = 1, pageSize = 10 } = _this.currentPagination;
const page = current - 1;
return page * pageSize + index;
});

// 合并 column
const mergeColumn = useMemoizedFn((column: EditableColumnType<RecordType>) => {
const { render: _render = v => v, dataIndex, isEditable = true, renderFormItem, formItemProps = {}} = column;

return {
...column,
render: (currentValue, record, index) => {
const currentRowKey = getRowKey(record, index);
const isValidDataIndex = isValidNamePath(dataIndex);
const rowNameIndex = getNameIndex(index);
const rowNamePath = isValidDataIndex ? [...getNamePath(name), rowNameIndex, ...getNamePath(dataIndex)] : [];
const extraParams: EditableColumnExtraRenderParams = { form, rowNameIndex, rowNamePath, tableNamePath };

if (!readonly && isEditable && (isUndefined(editableRowKeys) || editableRowKeys.includes(currentRowKey))) {
const formItemComponent = isFunction(renderFormItem) ? renderFormItem(currentValue, record, index, extraParams) : defaultFormItem;

return isValidDataIndex ? (
<Form.Item name={rowNamePath} noStyle {...formItemProps}>
{formItemComponent}
</Form.Item>
) : (
formItemComponent
);
}

return _render(currentValue, record, index, extraParams);
},
};
});

const mergedColumns = useMemo(() => columns?.map(mergeColumn) as TableProps<RecordType>['columns'], [columns, readonly]);

return (
<Table
dataSource={value}
columns={mergedColumns}
rowKey={_rowKey}
pagination={pagination}
onChange={(currentPagination, filters, sorter, extra) => {
_this.currentPagination = currentPagination;
onTableChange?.(currentPagination, filters, sorter, extra);
}}
{...otherProps}
/>
);
};

const EditableTable = <RecordType extends AnyObject = AnyObject>(
props: EditableTableProps<RecordType> & {
formItemProps?: Omit<FormItemProps, 'name'>;
},
) => {
const { name, formItemProps = {}, ...otherProps } = props;

return (
<Form.Item name={name} noStyle {...formItemProps}>
<InternalEditableTable name={name} {...otherProps} />
</Form.Item>
);
};

if (process.env.NODE_ENV !== 'production') {
EditableTable.displayName = 'EditableTable';
}

export default EditableTable;
162 changes: 162 additions & 0 deletions packages/antd-plus/src/editable-table/demo/basic.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import type { SelectProps } from 'antd';
import { Button, Form, InputNumber, Select, Space, Switch, Tag } from 'antd';
import type { EditableColumnsType, EditableTableActionType } from '@orca-fe/antd-plus';
import { EditableTable } from '@orca-fe/antd-plus';
import { useRef, useState } from 'react';
import { OpenBox } from '@orca-fe/pocket';

interface DataType {
name: string;
age: number;
address: string;
tags?: string[];
}

const tagOptions: SelectProps['options'] = [
{
label: '标签A',
value: 'a',
},
{
label: '标签B',
value: 'b',
},
{
label: '标签C',
value: 'c',
},
{
label: '标签D',
value: 'd',
},
{
label: '标签E',
value: 'e',
},
];

const data: DataType[] = [
{
name: '张三',
age: 19,
address: '地址1地址1地址1',
tags: ['b', 'c'],
},
{
name: '李四',
age: 27,
address: '地址2地址2地址2',
tags: ['a'],
},
{
name: '王五',
age: 22,
address: '地址3地址3地址3',
tags: ['d', 'e'],
},
];

const initialValues = { list: data };

export default () => {
const [form] = Form.useForm();
const [valuesString, setValuesString] = useState(JSON.stringify(initialValues, null, 2));
const [valuesStringVisible, setValuesStringVisible] = useState(false);
const [readonly, setReadonly] = useState(false);
const actionRef = useRef<EditableTableActionType>();

const columns: EditableColumnsType<DataType> = [
{
title: '姓名',
dataIndex: 'name',
render: text => <a>{text}</a>,
},
{
title: '年龄',
dataIndex: 'age',
renderFormItem: () => <InputNumber min={0} placeholder="请输入" />,
},
{
title: '地址',
dataIndex: 'address',
},
{
title: '标签',
dataIndex: 'tags',
render: (_, { tags }) => <>{tags?.map(tag => <Tag key={tag}>{tagOptions.find(t => t.value === tag)?.label ?? '-'}</Tag>)}</>,
renderFormItem: () => <Select mode="multiple" options={tagOptions} placeholder="请选择" />,
},
{
title: '操作',
isEditable: false,
render: (_, record, index, extraParams) => (
<Space size="middle">
<a
onClick={() => {
if (readonly) {
form.setFieldValue(
'list',
(form.getFieldValue('list') ?? []).filter((_, i) => i !== extraParams.rowNameIndex),
);
} else {
actionRef.current?.removeEditRecord(index);
}
}}
>
删除
</a>
</Space>
),
},
];

return (
<div>
<Space style={{ marginBottom: 6 }}>
<Switch
checked={readonly}
onChange={(checked) => {
setReadonly(checked);
}}
/>
<span>只读模式</span>
</Space>
<Form
form={form}
initialValues={initialValues}
onValuesChange={(_, values) => {
setValuesString(JSON.stringify(values, null, 2));
}}
>
<EditableTable name="list" readonly={readonly} actionRef={actionRef} columns={columns} style={{ margin: '12px 0' }} />
</Form>
<Space>
<Button
onClick={() => {
actionRef.current?.addEditRecord();
}}
>
插入一行数据
</Button>
<Button
onClick={() => {
actionRef.current?.addEditRecord({}, 0);
}}
>
在第一行插入数据
</Button>
<Button
type="primary"
onClick={() => {
setValuesStringVisible(!valuesStringVisible);
}}
>
{valuesStringVisible ? '收起' : '查看表单数据'}
</Button>
</Space>
<OpenBox open={valuesStringVisible}>
<pre>{valuesString}</pre>
</OpenBox>
</div>
);
};
Loading