From cc004ad627c7eed0367dd6fa53201245aaba8841 Mon Sep 17 00:00:00 2001 From: zwkang Date: Fri, 29 Mar 2024 16:04:30 +0800 Subject: [PATCH] feat: add table component --- src/table/a.tsx | 0 src/table/table.tsx | 240 +++++++++++++++++++++++++++++++++++++ src/table/type.ts | 200 +++++++++++++++++++++++++++++++ src/table/useClassNames.ts | 193 +++++++++++++++++++++++++++++ 4 files changed, 633 insertions(+) create mode 100644 src/table/a.tsx create mode 100644 src/table/table.tsx create mode 100644 src/table/type.ts create mode 100644 src/table/useClassNames.ts diff --git a/src/table/a.tsx b/src/table/a.tsx new file mode 100644 index 00000000..e69de29b diff --git a/src/table/table.tsx b/src/table/table.tsx new file mode 100644 index 00000000..3e012362 --- /dev/null +++ b/src/table/table.tsx @@ -0,0 +1,240 @@ +import React, { CSSProperties, forwardRef, useMemo } from 'react'; +import useDefaultProps from 'tdesign-mobile-react/hooks/useDefaultProps'; +import Loading, { TdLoadingProps } from 'tdesign-mobile-react/loading'; +import cls from 'classnames'; +import useConfig from 'tdesign-mobile-react/_util/useConfig'; +import isFunction from 'lodash/isFunction'; +import get from 'lodash/get'; +import { BaseTableCellParams, TableRowData, TdBaseTableProps } from './type'; +import useClassName from './useClassNames'; + +export function formatCSSUnit(unit: string | number | undefined) { + if (!unit) return unit; + return isNaN(Number(unit)) ? unit : `${unit}px`; +} + +const defaultProps: TdBaseTableProps = { + columns: [], + data: [], + empty: '', + loading: undefined, + rowKey: 'id', + showHeader: true, + tableContentWidth: '', + tableLayout: 'fixed', + verticalAlign: 'middle', +}; + +const BaseTable = forwardRef((props, ref) => { + const { + loadingProps, + bordered, + maxHeight, + tableLayout, + stripe, + loading, + verticalAlign, + height, + tableContentWidth, + cellEmptyContent, + empty, + columns, + showHeader, + data, + } = useDefaultProps(props, defaultProps); + const defaultLoadingContent = useMemo(() => , [loadingProps]); + const { tableBaseClass, tableHeaderClasses, tableAlignClasses, tableLayoutClasses, tdEllipsisClass, tdAlignClasses } = + useClassName(); + const { classPrefix } = useConfig(); + // const name = useMemo(() => `${classPrefix}-table`, [classPrefix]); + + const tableClasses = useMemo( + () => + cls([ + tableBaseClass.table, + tableAlignClasses[verticalAlign] || 'middle', + { + [tableBaseClass.bordered]: bordered, + [tableBaseClass.striped]: stripe, + [tableBaseClass.loading]: loading, + }, + ]), + [tableAlignClasses, tableBaseClass, bordered, stripe, loading, verticalAlign], + ); + + const tableContentStyles: CSSProperties = useMemo( + () => ({ + height: formatCSSUnit(height), + maxHeight: formatCSSUnit(maxHeight), + }), + [height, maxHeight], + ); + + const tableElementStyles: CSSProperties = useMemo( + () => ({ width: formatCSSUnit(tableContentWidth) }), + [tableContentWidth], + ); + + const defaultColWidth = tableLayout === 'fixed' ? '80px' : undefined; + + const theadClasses = useMemo( + () => + cls([ + tableHeaderClasses.header, + { + // todo(zwkang): 这里的 boolean 判断 + [tableHeaderClasses.fixed]: Boolean(maxHeight || height), + [tableBaseClass.bordered]: bordered, + }, + ]), + [height, maxHeight, bordered, tableHeaderClasses.header, tableHeaderClasses.fixed, tableBaseClass.bordered], + ); + + const tbodyClasses = useMemo(() => cls([tableBaseClass.body]), [tableBaseClass.body]); + const ellipsisClasses = useMemo( + () => cls([`${classPrefix}-table__ellipsis`, `${classPrefix}-text-ellipsis`]), + [classPrefix], + ); + + const handleRowClick = (row: TableRowData, rowIndex: number, e: MouseEvent) => { + props.onRowClick?.({ row, index: rowIndex, e }); + }; + + const handleCellClick = (row: TableRowData, col: any, rowIndex: number, colIndex: number, e: MouseEvent) => { + props.onCellClick?.({ row, col, rowIndex, colIndex, e }); + }; + + const dynamicBaseTableClasses = useMemo(() => cls([tableClasses]), [tableClasses]); + + const tableElmClasses = useMemo(() => cls([tableLayoutClasses[tableLayout || 'fixed']]), []); + // const internalInstance = getCurrentInstance(); + const renderContentEmpty = useMemo(() => empty ?? null, [empty]); + // const renderCellEmptyContent = computed(() => renderTNode(internalInstance, 'cellEmptyContent')); + const renderCellEmptyContent = useMemo(() => cellEmptyContent ?? null, [cellEmptyContent]); + + const renderCell = ( + params: BaseTableCellParams, + cellEmptyContent?: TdBaseTableProps['cellEmptyContent'], + ) => { + const { col, row, rowIndex } = params; + if (col.colKey === 'serial-number') { + return rowIndex + 1; + } + if (isFunction(col.cell)) { + // return col.cell(h, params); + return col.cell(params); + } + // todo 逻辑待补全 + // if (context.slots[col.colKey]) { + // return context.slots[col.colKey](params); + // } + + // if (isString(col.cell) && context.slots?.[col.cell]) { + // return context.slots[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 loadingClasses = useMemo(() => cls([`${classPrefix}-table__loading--full`]), [classPrefix]); + const loadingContent = useMemo(() => loading ?? defaultLoadingContent ?? null, [loading, defaultLoadingContent]); + // todo(zwkang): fix event type + const onInnerVirtualScroll = (e: any) => { + props.onScroll?.({ e }); + }; + + const cols = columns.map((colItem) => ( + + )); + + const header = showHeader ? ( + + + {columns.map((itemTh, indexTh) => ( + +
{itemTh.title}
+ + ))} + + + ) : null; + + const secondBody = () => + data.map((row, rowIndex) => ( + handleRowClick(row, rowIndex, e)}> + {columns.map((col, colIndex) => ( + handleCellClick(row, col, rowIndex, colIndex, e)} + > + {/* {renderCell({ col, row, rowIndex }, renderCellEmptyContent)} */} +
+ {renderCell({ col, row, rowIndex, colIndex }, renderCellEmptyContent)} +
+ + ))} + + )); + const firstBody = () => ( + + +
{renderContentEmpty}
+ + + ); + + const subBody = () => { + if (!data.length && renderContentEmpty) return firstBody(); + if (data.length) return secondBody(); + return null; + }; + + const body = {subBody()}; + + return ( +
+
+ + {cols} + + {header} + + {body} +
+ {loadingContent ?
{loadingContent}
: null} +
+
+ ); +}); + +BaseTable.displayName = 'BaseTable'; + +export default BaseTable; diff --git a/src/table/type.ts b/src/table/type.ts new file mode 100644 index 00000000..8e8e2656 --- /dev/null +++ b/src/table/type.ts @@ -0,0 +1,200 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TNode, ClassName } from '../common'; +import { LoadingProps } from '../loading'; + +export interface TdBaseTableProps { + /** + * 是否显示表格边框 + * @default false + */ + bordered?: boolean; + /** + * 单元格数据为空时呈现的内容 + */ + cellEmptyContent?: string | TNode>; + /** + * 列配置,泛型 T 指表格数据类型 + * @default [] + */ + columns?: Array>; + /** + * 数据源,泛型 T 指表格数据类型 + * @default [] + */ + data?: Array; + /** + * 空表格呈现样式,支持全局配置 `GlobalConfigProvider` + * @default '' + */ + empty?: string | TNode; + /** + * 固定行(冻结行),示例:[M, N],表示冻结表头 M 行和表尾 N 行。M 和 N 值为 0 时,表示不冻结行 + */ + fixedRows?: Array; + /** + * 表格高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px。如果不是绝对固定表格高度,建议使用 `maxHeight` + */ + height?: string | number; + /** + * 加载中状态。值为 `true` 会显示默认加载中样式,可以通过 Function 和 插槽 自定义加载状态呈现内容和样式。值为 `false` 则会取消加载状态 + */ + loading?: boolean | TNode; + /** + * 透传加载组件全部属性 + */ + loadingProps?: Partial; + /** + * 表格最大高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px + */ + maxHeight?: string | number; + /** + * 唯一标识一行数据的字段名,来源于 `data` 中的字段。如果是字段嵌套多层,可以设置形如 `item.a.id` 的方法 + * @default 'id' + */ + rowKey: string; + /** + * 是否显示表头 + * @default true + */ + showHeader?: boolean; + /** + * 是否显示斑马纹 + * @default false + */ + stripe?: boolean; + /** + * 表格内容的总宽度,注意不是表格可见宽度。主要应用于 `table-layout: auto` 模式下的固定列显示。`tableContentWidth` 内容宽度的值必须大于表格可见宽度 + * @default '' + */ + tableContentWidth?: string; + /** + * 表格布局方式 + * @default fixed + */ + tableLayout?: 'auto' | 'fixed'; + /** + * 行内容上下方向对齐 + * @default middle + */ + verticalAlign?: 'top' | 'middle' | 'bottom'; + /** + * 单元格点击时触发 + */ + onCellClick?: (context: BaseTableCellEventContext) => void; + /** + * 行点击时触发,泛型 T 指表格数据类型 + */ + onRowClick?: (context: RowEventContext) => void; + /** + * 表格内容滚动时触发 + */ + onScroll?: (params: { e: WheelEvent }) => void; +} + +/** 组件实例方法 */ +export interface BaseTableInstanceFunctions { + /** + * 全部重新渲染表格 + */ + refreshTable: () => void; +} + +export interface BaseTableCol { + /** + * 列横向对齐方式 + * @default left + */ + align?: 'left' | 'right' | 'center'; + /** + * 自定义单元格渲染。值类型为 Function 表示以函数形式渲染单元格。值类型为 string 表示使用插槽渲染,插槽名称为 cell 的值。默认使用 colKey 作为插槽名称。优先级高于 render。泛型 T 指表格数据类型 + */ + cell?: string | TNode>; + /** + * 渲染列所需字段,值为 `serial-number` 表示当前列为「序号」列 + * @default '' + */ + colKey?: string; + /** + * 单元格和表头内容超出时,是否显示省略号。如果仅希望单元格超出省略,可设置 `ellipsisTitle = false`。
值为 `true`,则超出省略浮层默认显示单元格内容;
值类型为 `Function` 则自定义超出省略浮中层显示的内容;
值类型为 `Object`,则自动透传属性到 Tooltip 组件,可用于调整浮层背景色和方向等特性。
同时透传 Tooltip 属性和自定义浮层内容,请使用 `{ props: { theme: 'light' }, content: () => 'something' }` + * @default false + */ + ellipsis?: boolean | TNode>; + /** + * 表头内容超出时,是否显示省略号。优先级高于 `ellipsis`。
值为 `true`,则超出省略的浮层默认显示表头全部内容;
值类型为 `Function` 用于自定义超出省略浮层显示的表头内容;
值类型为 `Object`,则自动透传属性到 Tooltip 组件,则自动透传属性到 Tooltip 组件,可用于调整浮层背景色和方向等特性。
同时透传 Tooltip 属性和自定义浮层内容,请使用 `{ props: { theme: 'light' }, content: () => 'something' }` + */ + ellipsisTitle?: boolean | TNode>; + /** + * 固定列显示位置 + * @default left + */ + fixed?: 'left' | 'right'; + /** + * 透传 CSS 属性 `min-width` 到 `` 元素。⚠️ 仅少部分浏览器支持,如:使用 [TablesNG](https://docs.google.com/document/d/16PFD1GtMI9Zgwu0jtPaKZJ75Q2wyZ9EZnVbBacOfiNA/preview) 渲染的 Chrome 浏览器支持 `minWidth` + */ + minWidth?: string | number; + /** + * 自定义表头渲染。值类型为 Function 表示以函数形式渲染表头。值类型为 string 表示使用插槽渲染,插槽名称为 title 的值。优先级高于 render + */ + title?: string | TNode<{ col: BaseTableCol; colIndex: number }>; + /** + * 列宽,可以作为最小宽度使用。当列宽总和小于 `table` 元素时,浏览器根据宽度设置情况自动分配宽度;当列宽总和大于 `table` 元素,表现为定宽。可以同时调整 `table` 元素的宽度来达到自己想要的效果 + */ + width?: string | number; +} + +export interface BaseTableCellEventContext { + row: T; + col: BaseTableCol; + rowIndex: number; + colIndex: number; + e: MouseEvent; +} + +export interface RowEventContext { + row: T; + index: number; + e: MouseEvent; +} + +export interface TableRowData { + [key: string]: any; + children?: TableRowData[]; +} + +export interface BaseTableCellParams { + row: T; + rowIndex: number; + col: BaseTableCol; + colIndex: number; +} + +export interface BaseTableColParams { + col: BaseTableCol; + colIndex: number; +} + +export interface RowClassNameParams { + row: T; + rowIndex: number; + type?: 'body' | 'foot'; +} + +export type TableColumnClassName = ClassName | ((context: CellData) => ClassName); + +export interface CellData extends BaseTableCellParams { + type: 'th' | 'td'; +} + +export interface BaseTableCellParams { + row: T; + rowIndex: number; + col: BaseTableCol; + colIndex: number; +} + +export type DataType = TableRowData; diff --git a/src/table/useClassNames.ts b/src/table/useClassNames.ts new file mode 100644 index 00000000..483a20ff --- /dev/null +++ b/src/table/useClassNames.ts @@ -0,0 +1,193 @@ +import { useMemo } from 'react'; +import useConfig from 'tdesign-mobile-react/_util/useConfig'; + +export default function useClassName() { + const { classPrefix } = useConfig(); + const classNames = useMemo( + () => ({ + classPrefix, + tableBaseClass: { + table: `${classPrefix}-table`, + columnResizableTable: `${classPrefix}-table--column-resizable`, + overflowVisible: `${classPrefix}-table--overflow-visible`, + body: `${classPrefix}-table__body`, + content: `${classPrefix}-table__content`, + topContent: `${classPrefix}-table__top-content`, + bottomContent: `${classPrefix}-table__bottom-content`, + paginationWrap: `${classPrefix}-table__pagination-wrap`, + tdLastRow: `${classPrefix}-table__td-last-row`, + tdFirstCol: `${classPrefix}-table__td-first-col`, + thCellInner: `${classPrefix}-table__th-cell-inner`, + tableRowEdit: `${classPrefix}-table--row-edit`, + cellEditable: `${classPrefix}-table__cell--editable`, + cellEditWrap: `${classPrefix}-table__cell-wrap`, + bordered: `${classPrefix}-table--bordered`, + striped: `${classPrefix}-table--striped`, + hover: `${classPrefix}-table--hoverable`, + loading: `${classPrefix}-table--loading`, + rowspanAndColspan: `${classPrefix}-table--rowspan-colspan`, + empty: `${classPrefix}-table__empty`, + emptyRow: `${classPrefix}-table__empty-row`, + headerFixed: `${classPrefix}-table--header-fixed`, + columnFixed: `${classPrefix}-table--column-fixed`, + widthOverflow: `${classPrefix}-table--width-overflow`, + multipleHeader: `${classPrefix}-table--multiple-header`, + footerAffixed: `${classPrefix}-table--footer-affixed`, + horizontalBarAffixed: `${classPrefix}-table--horizontal-bar-affixed`, + affixedHeader: `${classPrefix}-table--affixed-header`, + affixedHeaderElm: `${classPrefix}-table__affixed-header-elm`, + affixedFooterElm: `${classPrefix}-table__affixed-footer-elm`, + affixedFooterWrap: `${classPrefix}-table__affixed-footer-wrap`, + // 边框模式,固定表头,横向滚动时,右侧添加边线,分隔滚动条 + scrollbarDivider: `${classPrefix}-table__scroll-bar-divider`, + // 当用户设置 height 为固定高度,为保证行元素铺满 table,则需设置 table 元素高度为 100% + fullHeight: `${classPrefix}-table--full-height`, + // 拖拽列时的标记线 + resizeLine: `${classPrefix}-table__resize-line`, + obviousScrollbar: `${classPrefix}-table__scrollbar--obvious`, + affixedHeaderWrap: `${classPrefix}-table__affixed-header-elm-wrap`, + }, + + tdAlignClasses: { + left: `${classPrefix}-align-left`, + right: `${classPrefix}-align-right`, + center: `${classPrefix}-align-center`, + }, + + tableHeaderClasses: { + header: `${classPrefix}-table__header`, + thBordered: `${classPrefix}-table__header-th--bordered`, + fixed: `${classPrefix}-table__header--fixed`, + multipleHeader: `${classPrefix}-table__header--multiple`, + }, + + tableFooterClasses: { + footer: `${classPrefix}-table__footer`, + fixed: `${classPrefix}-table__footer--fixed`, + }, + + tableAlignClasses: { + top: `${classPrefix}-vertical-align-top`, + middle: `${classPrefix}-vertical-align-middle`, + bottom: `${classPrefix}-vertical-align-bottom`, + }, + + tableRowFixedClasses: { + top: `${classPrefix}-table__row--fixed-top`, + bottom: `${classPrefix}-table__row--fixed-bottom`, + firstBottom: `${classPrefix}-table__row--fixed-bottom-first`, + withoutBorderBottom: `${classPrefix}-table__row--without-border-bottom`, + }, + + tableColFixedClasses: { + left: `${classPrefix}-table__cell--fixed-left`, + right: `${classPrefix}-table__cell--fixed-right`, + lastLeft: `${classPrefix}-table__cell--fixed-left-last`, + firstRight: `${classPrefix}-table__cell--fixed-right-first`, + leftShadow: `${classPrefix}-table__content--scrollable-to-left`, + rightShadow: `${classPrefix}-table__content--scrollable-to-right`, + }, + + tableLayoutClasses: { + auto: `${classPrefix}-table--layout-auto`, + fixed: `${classPrefix}-table--layout-fixed`, + }, + + tdEllipsisClass: `${classPrefix}-table-td--ellipsis`, + + // 行通栏,一列铺满整行 + tableFullRowClasses: { + base: `${classPrefix}-table__row--full`, + innerFullRow: `${classPrefix}-table__row-full-inner`, + innerFullElement: `${classPrefix}-table__row-full-element`, + firstFullRow: `${classPrefix}-table__first-full-row`, + lastFullRow: `${classPrefix}-table__last-full-row`, + }, + + // 展开/收起行,全部类名 + tableExpandClasses: { + iconBox: `${classPrefix}-table__expand-box`, + iconCell: `${classPrefix}-table__expandable-icon-cell`, + row: `${classPrefix}-table__expanded-row`, + rowInner: `${classPrefix}-table__expanded-row-inner`, + expanded: `${classPrefix}-table__row--expanded`, + collapsed: `${classPrefix}-table__row--collapsed`, + }, + + // 排序功能,全部类名 + tableSortClasses: { + sortable: `${classPrefix}-table__cell--sortable`, + sortColumn: `${classPrefix}-table__sort-column`, + title: `${classPrefix}-table__cell--title`, + trigger: `${classPrefix}-table__cell--sort-trigger`, + doubleIcon: `${classPrefix}-table__double-icons`, + sortIcon: `${classPrefix}-table__sort-icon`, + iconDirection: { + asc: `${classPrefix}-table-sort-asc`, + desc: `${classPrefix}-table-sort-desc`, + }, + iconActive: `${classPrefix}-table__sort-icon--active`, + iconDefault: `${classPrefix}-icon-sort--default`, + }, + + // 行选中功能,全部类名 + tableSelectedClasses: { + selected: `${classPrefix}-table__row--selected`, + disabled: `${classPrefix}-table__row--disabled`, + checkCell: `${classPrefix}-table__cell-check`, + }, + + // 过滤功能,全部类名 + tableFilterClasses: { + filterable: `${classPrefix}-table__cell--filterable`, + popup: `${classPrefix}-table__filter-pop`, + icon: `${classPrefix}-table__filter-icon`, + popupContent: `${classPrefix}-table__filter-pop-content`, + result: `${classPrefix}-table__filter-result`, + inner: `${classPrefix}-table__row-filter-inner`, + bottomButtons: `${classPrefix}-table__filter--bottom-buttons`, + contentInner: `${classPrefix}-table__filter-pop-content-inner`, + iconWrap: `${classPrefix}-table__filter-icon-wrap`, + }, + + // 通用类名 + asyncLoadingClass: `${classPrefix}-table__async-loading`, + isFocusClass: `${classPrefix}-is-focus`, + isLoadingClass: `${classPrefix}-is-loading`, + isLoadMoreClass: `${classPrefix}-is-load-more`, + + // 树形结构类名 + tableTreeClasses: { + col: `${classPrefix}-table__tree-col`, + inlineCol: `${classPrefix}-table__tree-col--inline`, + icon: `${classPrefix}-table__tree-op-icon`, + leafNode: `${classPrefix}-table__tree-leaf-node`, + }, + + // 拖拽功能类名 + tableDraggableClasses: { + rowDraggable: `${classPrefix}-table--row-draggable`, + rowHandlerDraggable: `${classPrefix}-table--row-handler-draggable`, + colDraggable: `${classPrefix}-table--col-draggable`, + handle: `${classPrefix}-table__handle-draggable`, + ghost: `${classPrefix}-table__ele--draggable-ghost`, + chosen: `${classPrefix}-table__ele--draggable-chosen`, + dragging: `${classPrefix}-table__ele--draggable-dragging`, + dragSortTh: `${classPrefix}-table__th--drag-sort`, + }, + + virtualScrollClasses: { + cursor: `${classPrefix}-table__virtual-scroll-cursor`, + header: `${classPrefix}-table__virtual-scroll-header`, + }, + + positiveRotate90: `${classPrefix}-positive-rotate-90`, + negativeRotate180: `${classPrefix}-negative-rotate-180`, + }), + [classPrefix], + ); + + return classNames; +} + +export type TableClassName = ReturnType;