Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(table): 新增 Table 组件 #472

Merged
merged 7 commits into from
Aug 26, 2024
Merged
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
5 changes: 5 additions & 0 deletions site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -227,5 +227,10 @@ export default {
name: 'link',
component: () => import('tdesign-mobile-react/link/_example/index.tsx'),
},
{
title: 'Table 表格',
name: 'table',
component: () => import('tdesign-mobile-react/table/_example/index.jsx'),
},
],
};
6 changes: 6 additions & 0 deletions site/web/site.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,12 @@ export default {
path: '/mobile-react/components/tag',
component: () => import('tdesign-mobile-react/tag/tag.md'),
},
{
title: 'Table 表格',
name: 'table',
path: '/mobile-react/components/table',
component: () => import('tdesign-mobile-react/table/table.md'),
},
],
},
{
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export * from './swiper';
export * from './swipe-cell';
export * from './tag';
export * from './result';
export * from './table';

/**
* 消息提醒(7个)
Expand Down
210 changes: 210 additions & 0 deletions src/table/BaseTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
import React, { forwardRef, useRef } from 'react';
import isFunction from 'lodash/isFunction';
import get from 'lodash/get';
import cx from 'classnames';

import { StyledProps } from '../common';
import useClassName from './hooks/useClassName';
import useStyle, { formatCSSUnit } from './hooks/useStyle';
import useDefaultProps from '../hooks/useDefaultProps';
import defaultConfig from '../_common/js/global-config/mobile/locale/zh_CN';
import Loading from '../loading';
import { baseTableDefaultProps } from './defaultProps';

import type { TdBaseTableProps, BaseTableCol, TableRowData, BaseTableCellParams } from './type';

export type BaseTableProps = TdBaseTableProps & StyledProps;

export const BaseTable = forwardRef((props: BaseTableProps, ref: React.Ref<HTMLTableElement>) => {
anlyyao marked this conversation as resolved.
Show resolved Hide resolved
const {
data,
empty,
height,
loading,
loadingProps,
columns,
bordered,
maxHeight,
tableLayout,
showHeader,
cellEmptyContent,
className,
style,
onRowClick,
onCellClick,
onScroll,
} = useDefaultProps<BaseTableProps>(props, baseTableDefaultProps);

const { tableLayoutClasses, tableHeaderClasses, tableBaseClass, tdAlignClasses, tdEllipsisClass, classPrefix } =
useClassName();

const { tableClasses, tableContentStyles, tableElementStyles } = useStyle(props);

const tableElmClasses = tableLayoutClasses[tableLayout || 'fixed'];

const theadClasses = cx(tableHeaderClasses.header, {
[tableHeaderClasses.fixed]: Boolean(maxHeight || height),
[tableBaseClass.bordered]: bordered,
});

const ellipsisClasses = cx([`${classPrefix}-table__ellipsis`, `${classPrefix}-text-ellipsis`]);

const defaultColWidth = tableLayout === 'fixed' ? '80px' : undefined;

const tableContentRef = useRef();

const tableElmRef = useRef();

const theadRef = useRef();

const colStyle = (colItem: BaseTableCol<TableRowData>) => ({
width: `${formatCSSUnit(colItem.width || defaultColWidth)}`,
minWidth: `${
!formatCSSUnit(colItem.width || defaultColWidth) && !colItem.minWidth && tableLayout === 'fixed'
? '80px'
: formatCSSUnit(colItem.minWidth)
}`,
});

const thClassName = (thItem: BaseTableCol<TableRowData>) => {
let className = '';
if (thItem.colKey) {
className = `${classPrefix}-table__th-${thItem.colKey}`;
}
if (thItem.ellipsisTitle || thItem.ellipsis) {
className = `${className} ${tdEllipsisClass}`;
}
if (thItem.align && thItem.align !== 'left') {
className = `${className} ${tdAlignClasses[`${thItem.align}`]}`;
}
return className;
};

const tdClassName = (tdItem: BaseTableCol<TableRowData>) => {
let className = '';
if (tdItem.ellipsis) {
className = tdEllipsisClass;
}
if (tdItem.align && tdItem.align !== 'left') {
className = `${className} ${tdAlignClasses[`${tdItem.align}`]}`;
}
return className;
};

const renderCell = (
params: BaseTableCellParams<TableRowData>,
cellEmptyContent?: TdBaseTableProps['cellEmptyContent'],
) => {
const { col, row, rowIndex } = params;
// support serial number column
if (col.colKey === 'serial-number') {
return rowIndex + 1;
}

if (isFunction(col.cell)) {
return col.cell(params);
}

const r = get(row, col.colKey);
// 0 和 false 属于正常可用值,不能使用兜底逻辑 cellEmptyContent
if (![undefined, '', null].includes(r)) return r;

// cellEmptyContent 作为空数据兜底显示,用户可自定义
if (cellEmptyContent) {
return isFunction(cellEmptyContent) ? cellEmptyContent(params) : cellEmptyContent;
}
return r;
};

const renderTitle = (thItem: BaseTableCol<TableRowData>, index: number) => {
if (isFunction(thItem?.title)) {
return thItem?.title({ col: thItem, colIndex: index });
}
return thItem?.title;
};

const handleRowClick = (row: TableRowData, rowIndex: number, e: React.MouseEvent) => {
onRowClick?.({ row, index: rowIndex, e });
};

const handleCellClick = (row: TableRowData, col: any, rowIndex: number, colIndex: number, e: React.MouseEvent) => {
if (col.stopPropagation) {
e.stopPropagation();
}
onCellClick?.({ row, col, rowIndex, colIndex, e });
};

const renderTableBody = () => {
const renderContentEmpty = empty || defaultConfig?.table?.empty;

if (!data?.length && renderContentEmpty) {
return (
<tr className={tableBaseClass.emptyRow}>
<td colSpan={columns?.length}>
<div className={tableBaseClass.empty}>{renderContentEmpty}</div>
</td>
</tr>
);
}
if (data?.length) {
return data?.map((trItem, trIdx) => (
<tr
key={trIdx}
onClick={(ev) => {
handleRowClick(trItem, trIdx, ev);
}}
>
{columns?.map((tdItem, tdIdx) => (
<td
key={tdIdx}
className={tdClassName(tdItem)}
onClick={($event) => {
handleCellClick(trItem, tdItem, trIdx, tdIdx, $event);
}}
>
<div className={tdItem.ellipsis && ellipsisClasses}>
{renderCell({ row: trItem, col: tdItem, rowIndex: trIdx, colIndex: tdIdx }, cellEmptyContent)}
</div>
</td>
))}
</tr>
));
}
};

return (
<div ref={ref} className={cx(tableClasses, className)} style={{ position: 'relative', ...style }}>
<div
ref={tableContentRef}
className={tableBaseClass.content}
style={tableContentStyles}
onScroll={(e) => {
onScroll?.({ e });
}}
>
<table ref={tableElmRef} className={tableElmClasses} style={tableElementStyles}>
<colgroup>{columns?.map((col) => <col key={col.colKey} style={colStyle(col)} />)}</colgroup>
{showHeader && (
<thead ref={theadRef} className={theadClasses}>
<tr>
{columns?.map((thItem, idx) => (
<th key={idx} className={thClassName(thItem)}>
<div className={(thItem.ellipsisTitle || thItem.ellipsis) && ellipsisClasses}>
{renderTitle(thItem, idx)}
</div>
</th>
))}
</tr>
</thead>
)}
<tbody className={tableBaseClass.body}>{renderTableBody()}</tbody>
</table>
{loading && (
<div className={`${classPrefix}-table__loading--full`}>
<Loading {...loadingProps} />
</div>
)}
</div>
</div>
);
});
61 changes: 61 additions & 0 deletions src/table/_example/base.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import React from 'react';
import { Table } from 'tdesign-mobile-react';

const data = [];
const total = 10;
for (let i = 0; i < total; i++) {
data.push({
index: i + 1,
applicant: ['内容', '内容', '内容'][i % 3],
status: ['内容', '内容', '内容'][i % 3],
channel: ['内容', '内容', '内容'][i % 3],
detail: {
email: ['内容', '内容', '内容内容内容'][i % 3],
},
});
}

const columns = [
{ colKey: 'applicant', title: '标题', ellipsis: true, cell: 'type-slot-name' },
{
colKey: 'status',
title: '标题',
ellipsis: true,
},
{
colKey: 'channel',
title: '标题',
cell: ({ col, row }) => row[col.colKey],
ellipsis: true,
},
{
colKey: 'detail.email',
title: '标题',
cell: () => '内容',
ellipsis: true,
},
];

export function BaseExample() {
const handleRowClick = (e) => {
console.log('row-cliek=====', e);
};

const handleCellClick = (e) => {
console.log('cell-cliek=====', e);
};

return (
<div style={{ margin: '16px 16px 0' }}>
<Table
columns={columns}
data={data}
cellEmptyContent={'vvv'}
rowKey="index"
showHeader
onCellClick={handleCellClick}
onRowClick={handleRowClick}
></Table>
</div>
);
}
56 changes: 56 additions & 0 deletions src/table/_example/bordered.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import { Table } from 'tdesign-mobile-react';

const data = [];
const total = 10;
for (let i = 0; i < total; i++) {
data.push({
index: i + 1,
applicant: ['内容', '内容', '内容'][i % 3],
status: ['内容', '内容', '内容'][i % 3],
channel: ['内容', '内容', '内容'][i % 3],
detail: {
email: ['内容', '内容', '内容内容内容'][i % 3],
},
});
}

const columns = [
{ colKey: 'applicant', title: '标题', ellipsis: true },
{
colKey: 'status',
title: '标题',
ellipsis: true,
},
{ colKey: 'channel', title: '标题', ellipsis: true },
{ colKey: 'detail.email', title: '标题', ellipsis: true },
];

export function BorderedExample() {
const handleRowClick = (e) => {
console.log('row-cliek=====', e);
};

const handleCellClick = (e) => {
console.log('cell-cliek=====', e);
};

const handleScroll = (e) => {
console.log('scroll=====', e);
};

return (
<div style={{ margin: '16px 16px 0' }}>
<Table
columns={columns}
data={data}
rowKey="index"
showHeader
onCellClick={handleCellClick}
onRowClick={handleRowClick}
onScroll={handleScroll}
bordered
></Table>
</div>
);
}
30 changes: 30 additions & 0 deletions src/table/_example/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from 'react';
import TDemoBlock from '../../../site/mobile/components/DemoBlock';
import TDemoHeader from '../../../site/mobile/components/DemoHeader';
import { BaseExample } from './base';
import { ScrollExample } from './scroll';
import { StripeExample } from './stripe';
import { BorderedExample } from './bordered';

export default function Base() {
return (
<div className="tdesign-mobile-demo">
<TDemoHeader
title="Table 表格"
summary=" 表格常用于展示同类结构下的多种数据,易于组织、对比和分析等,并可对数据进行搜索、筛选、排序等操作。一般包括表头、数据行和表尾三部分。 "
/>
<TDemoBlock title="01 组件类型" summary="基础表格">
<BaseExample />

<TDemoHeader title="" summary="横向平铺可滚动表格" />
<ScrollExample />

<TDemoHeader title="" summary="带斑马纹表格样式" />
<StripeExample />

<TDemoHeader title="" summary="带边框表格样式" />
<BorderedExample />
</TDemoBlock>
</div>
);
}
Loading
Loading