feat: custom table with row children

This commit is contained in:
jialin
2024-06-12 09:26:17 +08:00
parent f491b5dfb1
commit 405d95c81f
19 changed files with 611 additions and 12 deletions
@@ -0,0 +1,27 @@
import classNames from 'classnames';
import React, { useContext } from 'react';
import RowContext from '../row-context';
import '../styles/cell.less';
import { SealColumnProps } from '../types';
const SealColumn: React.FC<SealColumnProps> = (props) => {
const row: Record<string, any> = useContext(RowContext);
const { dataIndex, render, align } = props;
return (
<div
className={classNames('cell', {
'cell-left': align === 'left',
'cell-center': align === 'center',
'cell-right': align === 'right'
})}
>
<span className="cell-content">
{render
? render(row[dataIndex], { ...row, dataIndex })
: row[dataIndex]}
</span>
</div>
);
};
export default React.memo(SealColumn);
@@ -0,0 +1,24 @@
import classNames from 'classnames';
import React from 'react';
import '../styles/header.less';
import { TableHeaderProps } from '../types';
const TableHeader: React.FC<TableHeaderProps> = (props) => {
const { title, style, align, firstCell, lastCell } = props;
return (
<div
style={{ ...style }}
className={classNames('table-header', {
'table-header-left': align === 'left',
'table-header-center': align === 'center',
'table-header-right': align === 'right',
'table-header-first': firstCell,
'table-header-last': lastCell
})}
>
<div className="table-header-cell">{title}</div>
</div>
);
};
export default React.memo(TableHeader);
@@ -0,0 +1,137 @@
import { DownOutlined, RightOutlined } from '@ant-design/icons';
import { Button, Checkbox, Col, Row } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import React, { useEffect, useState } from 'react';
import RowContext from '../row-context';
import { RowContextProps, SealTableProps } from '../types';
const TableRow: React.FC<
RowContextProps &
Omit<SealTableProps, 'dataSource' | 'loading' | 'children' | 'empty'>
> = (props) => {
const {
record,
rowIndex,
expandable,
rowSelection,
rowKey,
columns,
onExpand
} = props;
const [expanded, setExpanded] = useState(false);
const [checked, setChecked] = useState(false);
useEffect(() => {
if (rowSelection) {
const { selectedRowKeys } = rowSelection;
if (selectedRowKeys.includes(record[rowKey])) {
setChecked(true);
} else {
setChecked(false);
}
}
}, [rowSelection]);
const handleRowExpand = () => {
setExpanded(!expanded);
onExpand?.(!expanded, record);
};
const handleSelectChange = (e: any) => {
if (e.target.checked) {
// update selectedRowKeys
rowSelection?.onChange(
_.uniq([...rowSelection?.selectedRowKeys, record[rowKey]])
);
} else {
// update selectedRowKeys
rowSelection?.onChange(
rowSelection?.selectedRowKeys.filter((key) => key !== record[rowKey])
);
}
};
const renderRowPrefix = () => {
if (expandable && rowSelection) {
return (
<div className="row-prefix-wrapper">
<span style={{ marginRight: 5 }}>
{_.isBoolean(expandable) ? (
<Button type="text" size="small" onClick={handleRowExpand}>
{expanded ? <DownOutlined /> : <RightOutlined />}
</Button>
) : (
expandable
)}
</span>
<Checkbox onChange={handleSelectChange} checked={checked}></Checkbox>
</div>
);
}
if (expandable) {
return (
<div className="row-prefix-wrapper">
{_.isBoolean(expandable) ? (
<Button type="text" size="small" onClick={handleRowExpand}>
{expanded ? <DownOutlined /> : <RightOutlined />}
</Button>
) : (
expandable
)}
</div>
);
}
if (rowSelection) {
return (
<div className="row-prefix-wrapper">
{
<Checkbox
onChange={handleSelectChange}
checked={checked}
></Checkbox>
}
</div>
);
}
return null;
};
return (
<RowContext.Provider value={{ ...record, rowIndex }}>
<div className="row-box">
<div
className={classNames('row-wrapper', {
'row-wrapper-selected': checked
})}
>
{renderRowPrefix()}
<Row className="seal-table-row">
{React.Children.map(columns, (child) => {
const { props: columnProps } = child as any;
if (React.isValidElement(child)) {
return (
<Col
key={`${columnProps.dataIndex}-${rowIndex}`}
span={columnProps.span}
>
{child}
</Col>
);
}
return <Col span={columnProps.span}></Col>;
})}
</Row>
</div>
{expanded && (
<div className="expanded-row">
<div className="expanded-row-content">rowchildren</div>
</div>
)}
</div>
</RowContext.Provider>
);
};
export default TableRow;
+145
View File
@@ -0,0 +1,145 @@
import { RightOutlined } from '@ant-design/icons';
import { Button, Checkbox, Col, Empty, Row, Spin } from 'antd';
import _ from 'lodash';
import React, { useEffect, useState } from 'react';
import TableHeader from './components/table-header';
import TableRow from './components/table-row';
import './styles/index.less';
import { SealColumnProps, SealTableProps } from './types';
const SealTable: React.FC<SealTableProps> = (props) => {
const { children, rowKey, onExpand, loading, expandable, rowSelection } =
props;
const [selectAll, setSelectAll] = useState(false);
const [indeterminate, setIndeterminate] = useState(false);
useEffect(() => {
if (rowSelection) {
const { selectedRowKeys } = rowSelection;
if (selectedRowKeys.length === 0) {
setSelectAll(false);
setIndeterminate(false);
} else if (selectedRowKeys.length === props.dataSource.length) {
setSelectAll(true);
setIndeterminate(false);
} else {
setSelectAll(false);
setIndeterminate(true);
}
}
}, [rowSelection]);
const handleSelectAllChange = (e: any) => {
if (e.target.checked) {
// update selectedRowKeys
rowSelection?.onChange(props.dataSource.map((record) => record[rowKey]));
setSelectAll(true);
setIndeterminate(false);
} else {
// update selectedRowKeys
rowSelection?.onChange([]);
setSelectAll(false);
setIndeterminate(false);
}
};
const renderHeaderPrefix = () => {
if (expandable && rowSelection) {
return (
<div className="header-row-prefix-wrapper">
<span style={{ marginRight: 5, padding: '0 14px' }}></span>
<Checkbox
onChange={handleSelectAllChange}
indeterminate={indeterminate}
checked={selectAll}
></Checkbox>
</div>
);
}
if (expandable) {
return (
<div className="header-row-prefix-wrapper">
{_.isBoolean(expandable) ? (
<Button type="text" size="small">
<RightOutlined />
</Button>
) : (
expandable
)}
</div>
);
}
if (rowSelection) {
return (
<div className="header-row-prefix-wrapper">{<Checkbox></Checkbox>}</div>
);
}
return null;
};
const renderHeader = () => {
return (
<div className="header-row-wrapper">
{renderHeaderPrefix()}
<Row className="row">
{React.Children.map(props.children, (child, i) => {
const { props: columnProps } = child as any;
const { title, align, span, headerStyle } =
columnProps as SealColumnProps;
if (React.isValidElement(child)) {
return (
<Col span={span}>
<TableHeader
title={title}
style={headerStyle}
firstCell={i === 0}
lastCell={i === props.children.length - 1}
></TableHeader>
</Col>
);
}
return null;
})}
</Row>
</div>
);
};
const renderContent = () => {
if (!props.dataSource.length) {
return <Empty></Empty>;
}
return (
<div className="seal-table-content">
{props.dataSource.map((item, index) => {
return (
<TableRow
record={item}
rowIndex={index}
columns={children}
key={item[rowKey]}
rowSelection={rowSelection}
expandable={expandable}
rowKey={rowKey}
onExpand={onExpand}
></TableRow>
);
})}
</div>
);
};
return (
<div className="seal-table-container">
{renderHeader()}
{loading ? (
<div className="spin">
<Spin></Spin>
</div>
) : (
renderContent()
)}
</div>
);
};
export default SealTable;
+5
View File
@@ -0,0 +1,5 @@
import React from 'react';
const RowContext = React.createContext({});
export default RowContext;
@@ -0,0 +1,17 @@
.cell {
padding: var(--ant-table-cell-padding-block)
var(--ant-table-cell-padding-inline);
display: flex;
align-items: center;
justify-content: flex-start;
height: 70px;
&-left {
justify-content: flex-start;
}
&-right {
justify-content: flex-end;
}
&-center {
justify-content: center;
}
}
@@ -0,0 +1,23 @@
.table-header {
display: flex;
align-items: center;
justify-content: flex-start;
padding-inline: var(--ant-table-cell-padding-inline);
border-right: 1px solid var(--ant-table-header-split-color);
&-last {
border-right: none;
}
&-cell {
font-weight: 600;
}
&-left {
justify-content: flex-start;
}
&-right {
justify-content: flex-end;
}
&-center {
justify-content: center;
}
}
@@ -0,0 +1,64 @@
.seal-table-container {
display: flex;
flex-direction: column;
width: 100%;
.header-row-wrapper {
height: 40px;
margin-bottom: 10px;
display: flex;
justify-content: flex-start;
align-items: center;
.header-row-prefix-wrapper {
padding-left: var(--ant-table-cell-padding-inline);
}
.row {
flex: 1;
}
}
.row-box {
margin-bottom: 20px;
border-radius: var(--ant-table-header-border-radius);
overflow: hidden;
}
.expanded-row {
background-color: var(--color-white-1);
padding: 16px 20px;
border: 1px solid var(--color-fill-1);
border-top: 0;
border-radius: 0 0 var(--ant-table-header-border-radius)
var(--ant-table-header-border-radius);
}
.row-wrapper {
display: flex;
justify-content: flex-start;
align-items: center;
background-color: var(--color-fill-1);
transition: all 0.2s ease;
&:hover {
background-color: var(--ant-table-row-hover-bg);
transition: all 0.2s ease;
}
&-selected {
background-color: var(--ant-table-row-selected-bg);
transition: all 0.2s ease;
&:hover {
background-color: var(--ant-table-row-selected-hover-bg);
transition: all 0.2s ease;
}
}
.row-prefix-wrapper {
padding-left: var(--ant-table-cell-padding-inline);
}
}
.seal-table-row {
flex: 1;
}
.spin {
text-align: center;
display: flex;
justify-content: center;
align-items: center;
margin-top: 20px;
min-height: 100px;
}
}
+41
View File
@@ -0,0 +1,41 @@
import React from 'react';
export interface SealColumnProps {
title: string;
render?: (text: any, record: any) => React.ReactNode;
dataIndex: string;
width?: number;
span: number;
align?: 'left' | 'center' | 'right';
headerStyle?: React.CSSProperties;
}
export interface TableHeaderProps {
children?: React.ReactNode;
title: string;
style?: React.CSSProperties;
firstCell?: boolean;
lastCell?: boolean;
align?: 'left' | 'center' | 'right';
}
export interface RowSelectionProps {
selectedRowKeys: React.Key[];
onChange: (selectedRowKeys: React.Key[]) => void;
}
export interface SealTableProps {
rowSelection?: RowSelectionProps;
children: React.ReactNode[];
empty?: React.ReactNode;
expandable?: React.ReactNode;
dataSource: any[];
loading?: boolean;
onExpand?: (expanded: boolean, record: any) => void;
rowKey: string;
}
export interface RowContextProps {
record: Record<string, any>;
columns: React.ReactNode[];
rowIndex: number;
}