Skip to content

Commit

Permalink
feat(table): 新增 Table 组件 (#472)
Browse files Browse the repository at this point in the history
* feat(table): 新增 Table 组件

新增 Table 组件

* fix: fix(table): 修复comment问题

* fix: 修复comment问题

* docs(table): 更新Table md文档

* chore: update common

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
TianlunXiong and github-actions[bot] authored Aug 26, 2024
1 parent aa55b00 commit 7216ea9
Show file tree
Hide file tree
Showing 19 changed files with 999 additions and 1 deletion.
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>) => {
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

0 comments on commit 7216ea9

Please sign in to comment.