diff --git a/packages/components/table/README.en-US.md b/packages/components/table/README.en-US.md new file mode 100644 index 000000000..fb322a086 --- /dev/null +++ b/packages/components/table/README.en-US.md @@ -0,0 +1,53 @@ +:: BASE_DOC :: + +## API + +### BaseTable Props + +name | type | default | description | required +-- | -- | -- | -- | -- +style | Object | - | CSS(Cascading Style Sheets) | N +custom-style | Object | - | CSS(Cascading Style Sheets),used to set style on virtual component | N +bordered | Boolean | false | show table bordered | N +cell-empty-content | String | - | \- | N +columns | Array | [] | table column configs。Typescript: `Array>` | N +data | Array | [] | table data。Typescript: `Array` | N +empty | String | '' | empty text or empty element | N +fixed-rows | Array | - | Typescript: `Array` | N +height | String / Number | - | table height | N +loading | Boolean | undefined | loading state table | N +loading-props | Object | - | Typescript: `Partial`,[Loading API Documents](./loading?tab=api)。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/table/type.ts) | N +max-height | String / Number | - | table max height | N +row-key | String | 'id' | required。unique key for each row data | Y +show-header | Boolean | true | show table header | N +stripe | Boolean | false | show stripe style | N +table-content-width | String | - | \- | N +table-layout | String | fixed | table-layout css properties, [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/table-layout). set value to be `fixed` on `resizable=true` please。options: auto/fixed | N +vertical-align | String | middle | vertical align。options: top/middle/bottom | N + +### BaseTable Events + +name | params | description +-- | -- | -- +cell-click | `(context: BaseTableCellEventContext)` | trigger on cell clicked。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/table/type.ts)。
`interface BaseTableCellEventContext { row: T; col: BaseTableCol; rowIndex: number; colIndex: number;}`
+row-click | `(context: RowEventContext)` | trigger on row click。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/table/type.ts)。
`interface RowEventContext { row: T; index: number;}`
+ +### BaseTable Slots + +name | Description +-- | -- +cell-empty-content | \- +empty | empty text or empty element +loading | loading state table + +### BaseTableCol + +name | type | default | description | required +-- | -- | -- | -- | -- +align | String | left | align type。options: left/right/center | N +cell | String / Function | - | use cell to render table cell。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/table/type.ts) | N +class-name | String / Object / Array / Function | - | cell classnames。Typescript: `TableColumnClassName \| TableColumnClassName[]` `type TableColumnClassName = ClassName \| ((context: CellData) => ClassName)` `interface CellData extends BaseTableCellParams { type: 'th' \| 'td' }`。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/common/common.ts)。[see more ts definition](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/table/type.ts) | N +col-key | String | - | unique key for column | N +fixed | String | left | fixed current column to left or right。options: left/right | N +min-width | String / Number | - | add CSS property `min-width` to HTML Element ``,Browsers with [TablesNG](https://docs.google.com/document/d/16PFD1GtMI9Zgwu0jtPaKZJ75Q2wyZ9EZnVbBacOfiNA/preview) support `minWidth` | N +width | String / Number | - | column width | N diff --git a/packages/components/table/README.md b/packages/components/table/README.md new file mode 100644 index 000000000..28a23e772 --- /dev/null +++ b/packages/components/table/README.md @@ -0,0 +1,53 @@ +:: BASE_DOC :: + +## API + +### BaseTable Props + +名称 | 类型 | 默认值 | 描述 | 必传 +-- | -- | -- | -- | -- +style | Object | - | 样式 | N +custom-style | Object | - | 样式,一般用于开启虚拟化组件节点场景 | N +bordered | Boolean | false | 是否显示表格边框 | N +cell-empty-content | String | - | 单元格数据为空时呈现的内容 | N +columns | Array | [] | 列配置,泛型 T 指表格数据类型。TS 类型:`Array>` | N +data | Array | [] | 数据源,泛型 T 指表格数据类型。TS 类型:`Array` | N +empty | String | '' | 空表格呈现样式,支持全局配置 `GlobalConfigProvider` | N +fixed-rows | Array | - | 固定行(冻结行),示例:[M, N],表示冻结表头 M 行和表尾 N 行。M 和 N 值为 0 时,表示不冻结行。TS 类型:`Array` | N +height | String / Number | - | 表格高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px。如果不是绝对固定表格高度,建议使用 `maxHeight` | N +loading | Boolean | undefined | 加载中状态。值为 `true` 会显示默认加载中样式,可以通过 Function 和 插槽 自定义加载状态呈现内容和样式。值为 `false` 则会取消加载状态 | N +loading-props | Object | - | 透传加载组件全部属性。TS 类型:`Partial`,[Loading API Documents](./loading?tab=api)。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/table/type.ts) | N +max-height | String / Number | - | 表格最大高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px | N +row-key | String | 'id' | 必需。唯一标识一行数据的字段名,来源于 `data` 中的字段。如果是字段嵌套多层,可以设置形如 `item.a.id` 的方法 | Y +show-header | Boolean | true | 是否显示表头 | N +stripe | Boolean | false | 是否显示斑马纹 | N +table-content-width | String | - | 表格内容的总宽度,注意不是表格可见宽度。主要应用于 `table-layout: auto` 模式下的固定列显示。`tableContentWidth` 内容宽度的值必须大于表格可见宽度 | N +table-layout | String | fixed | 表格布局方式。可选项:auto/fixed | N +vertical-align | String | middle | 行内容上下方向对齐。可选项:top/middle/bottom | N + +### BaseTable Events + +名称 | 参数 | 描述 +-- | -- | -- +cell-click | `(context: BaseTableCellEventContext)` | 单元格点击时触发。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/table/type.ts)。
`interface BaseTableCellEventContext { row: T; col: BaseTableCol; rowIndex: number; colIndex: number;}`
+row-click | `(context: RowEventContext)` | 行点击时触发,泛型 T 指表格数据类型。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/table/type.ts)。
`interface RowEventContext { row: T; index: number;}`
+ +### BaseTable Slots + +名称 | 描述 +-- | -- +cell-empty-content | 自定义 `cell-empty-content` 显示内容 +empty | 自定义 `empty` 显示内容 +loading | 自定义 `loading` 显示内容 + +### BaseTableCol + +名称 | 类型 | 默认值 | 描述 | 必传 +-- | -- | -- | -- | -- +align | String | left | 列横向对齐方式。可选项:left/right/center | N +cell | String / Function | - | 自定义单元格渲染。默认使用 `colKey` 的值作为自定义当前列的插槽名称。
如果 `cell` 值类型为 Function 表示以函数形式渲染单元格。值类型为 string 表示使用插槽渲染,插槽名称为 cell 的值。优先级高于 `render`。泛型 T 指表格数据类型。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/table/type.ts) | N +class-name | String / Object / Array / Function | - | 列类名,值类型是 Function 使用返回值作为列类名;值类型不为 Function 时,值用于整列类名(含表头)。泛型 T 指表格数据类型。TS 类型:`TableColumnClassName \| TableColumnClassName[]` `type TableColumnClassName = ClassName \| ((context: CellData) => ClassName)` `interface CellData extends BaseTableCellParams { type: 'th' \| 'td' }`。[通用类型定义](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/common/common.ts)。[详细类型定义](https://github.com/Tencent/tdesign-miniprogram/blob/develop/packages/components/table/type.ts) | N +col-key | String | - | 渲染列所需字段,值为 `serial-number` 表示当前列为「序号」列 | N +fixed | String | left | 固定列显示位置。可选项:left/right | N +min-width | String / Number | - | 透传 CSS 属性 `min-width` 到 `` 元素。⚠️ 仅少部分浏览器支持,如:使用 [TablesNG](https://docs.google.com/document/d/16PFD1GtMI9Zgwu0jtPaKZJ75Q2wyZ9EZnVbBacOfiNA/preview) 渲染的 Chrome 浏览器支持 `minWidth` | N +width | String / Number | - | 列宽,可以作为最小宽度使用。当列宽总和小于 `table` 元素时,浏览器根据宽度设置情况自动分配宽度;当列宽总和大于 `table` 元素,表现为定宽。可以同时调整 `table` 元素的宽度来达到自己想要的效果 | N diff --git a/packages/components/table/_example/base/index.js b/packages/components/table/_example/base/index.js new file mode 100644 index 000000000..0eeea6cc2 --- /dev/null +++ b/packages/components/table/_example/base/index.js @@ -0,0 +1,41 @@ +import SkylineBehavior from '@behaviors/skyline.js'; + +Component({ + behaviors: [SkylineBehavior], + data: { + columns: [ + { colKey: 'applicant', title: '标题', ellipsis: true }, + { colKey: 'status', title: '标题', ellipsis: true }, + { colKey: 'channel', title: '标题', ellipsis: true }, + { colKey: 'detail.email', title: '标题', ellipsis: true }, + ], + data: [], + }, + lifetimes: { + attached() { + const data = []; + const total = 10; + for (let i = 0; i < total; i += 1) { + data.push({ + id: i + 1, + index: i + 1, + applicant: ['内容', '内容', '内容'][i % 3], + status: ['内容', '内容', '内容'][i % 3], + channel: ['内容', '内容', '内容'][i % 3], + detail: { + email: ['内容', '内容', '内容'][i % 3], + }, + }); + } + this.setData({ data }); + }, + }, + methods: { + handleRowClick(e) { + console.log('[row-click]', e.detail); + }, + handleCellClick(e) { + console.log('[cell-click]', e.detail); + }, + }, +}); diff --git a/packages/components/table/_example/base/index.json b/packages/components/table/_example/base/index.json new file mode 100644 index 000000000..e3c621db0 --- /dev/null +++ b/packages/components/table/_example/base/index.json @@ -0,0 +1,6 @@ +{ + "component": true, + "usingComponents": { + "t-table": "tdesign-miniprogram/table/table" + } +} diff --git a/packages/components/table/_example/base/index.wxml b/packages/components/table/_example/base/index.wxml new file mode 100644 index 000000000..dcd1ef5bd --- /dev/null +++ b/packages/components/table/_example/base/index.wxml @@ -0,0 +1,9 @@ + diff --git a/packages/components/table/_example/base/index.wxss b/packages/components/table/_example/base/index.wxss new file mode 100644 index 000000000..bc0171b96 --- /dev/null +++ b/packages/components/table/_example/base/index.wxss @@ -0,0 +1 @@ +/* base demo 无额外样式 */ diff --git a/packages/components/table/_example/bordered/index.js b/packages/components/table/_example/bordered/index.js new file mode 100644 index 000000000..85afad3f9 --- /dev/null +++ b/packages/components/table/_example/bordered/index.js @@ -0,0 +1,41 @@ +import SkylineBehavior from '@behaviors/skyline.js'; + +Component({ + behaviors: [SkylineBehavior], + data: { + columns: [ + { colKey: 'applicant', title: '标题', ellipsis: true }, + { colKey: 'status', title: '标题', ellipsis: true }, + { colKey: 'channel', title: '标题', ellipsis: true }, + { colKey: 'detail.email', title: '标题', ellipsis: true }, + ], + data: [], + }, + lifetimes: { + attached() { + const data = []; + const total = 10; + for (let i = 0; i < total; i += 1) { + data.push({ + id: i + 1, + index: i + 1, + applicant: ['内容', '内容', '内容'][i % 3], + status: ['内容', '内容', '内容'][i % 3], + channel: ['内容', '内容', '内容'][i % 3], + detail: { + email: ['内容', '内容', '内容内容内容'][i % 3], + }, + }); + } + this.setData({ data }); + }, + }, + methods: { + handleRowClick(e) { + console.log('[row-click]', e.detail); + }, + handleCellClick(e) { + console.log('[cell-click]', e.detail); + }, + }, +}); diff --git a/packages/components/table/_example/bordered/index.json b/packages/components/table/_example/bordered/index.json new file mode 100644 index 000000000..e3c621db0 --- /dev/null +++ b/packages/components/table/_example/bordered/index.json @@ -0,0 +1,6 @@ +{ + "component": true, + "usingComponents": { + "t-table": "tdesign-miniprogram/table/table" + } +} diff --git a/packages/components/table/_example/bordered/index.wxml b/packages/components/table/_example/bordered/index.wxml new file mode 100644 index 000000000..916811774 --- /dev/null +++ b/packages/components/table/_example/bordered/index.wxml @@ -0,0 +1,10 @@ + diff --git a/packages/components/table/_example/bordered/index.wxss b/packages/components/table/_example/bordered/index.wxss new file mode 100644 index 000000000..0a7416857 --- /dev/null +++ b/packages/components/table/_example/bordered/index.wxss @@ -0,0 +1 @@ +/* bordered demo 无额外样式 */ diff --git a/packages/components/table/_example/rowspan-colspan/index.js b/packages/components/table/_example/rowspan-colspan/index.js new file mode 100644 index 000000000..7be8f4474 --- /dev/null +++ b/packages/components/table/_example/rowspan-colspan/index.js @@ -0,0 +1,49 @@ +import SkylineBehavior from '@behaviors/skyline.js'; + +Component({ + behaviors: [SkylineBehavior], + data: { + columns: [ + { colKey: 'index', title: '序号', width: 60 }, + { colKey: 'name', title: '姓名' }, + { colKey: 'department', title: '部门' }, + { colKey: 'status', title: '状态' }, + ], + data: [ + { id: 1, index: 1, name: '张三', department: '研发部', status: '在职' }, + { id: 2, index: 2, name: '李四', department: '研发部', status: '在职' }, + { id: 3, index: 3, name: '王五', department: '设计部', status: '离职' }, + { id: 4, index: 4, name: '赵六', department: '产品部', status: '在职' }, + ], + // 注意:小程序中 rowspanAndColspan 需要通过函数传递 + // 合并规则: + // 1. 第1-2行的"部门"列合并(rowspan) + // 2. 第3行的"姓名"和"部门"列合并(colspan) + rowspanAndColspan: null, + }, + lifetimes: { + attached() { + this.setData({ + rowspanAndColspan: ({ rowIndex, colIndex }) => { + // 第1行第2列(部门):向下合并2行 + if (rowIndex === 0 && colIndex === 2) { + return { rowspan: 2, colspan: 1 }; + } + // 第2行第2列(部门):被上面合并,不渲染 + if (rowIndex === 1 && colIndex === 2) { + return { rowspan: 0, colspan: 0 }; + } + // 第3行第1列(姓名):向右合并2列 + if (rowIndex === 2 && colIndex === 1) { + return { rowspan: 1, colspan: 2 }; + } + // 第3行第2列(部门):被左边合并,不渲染 + if (rowIndex === 2 && colIndex === 2) { + return { rowspan: 0, colspan: 0 }; + } + return {}; + }, + }); + }, + }, +}); diff --git a/packages/components/table/_example/rowspan-colspan/index.json b/packages/components/table/_example/rowspan-colspan/index.json new file mode 100644 index 000000000..e3c621db0 --- /dev/null +++ b/packages/components/table/_example/rowspan-colspan/index.json @@ -0,0 +1,6 @@ +{ + "component": true, + "usingComponents": { + "t-table": "tdesign-miniprogram/table/table" + } +} diff --git a/packages/components/table/_example/rowspan-colspan/index.wxml b/packages/components/table/_example/rowspan-colspan/index.wxml new file mode 100644 index 000000000..571953fec --- /dev/null +++ b/packages/components/table/_example/rowspan-colspan/index.wxml @@ -0,0 +1,8 @@ + diff --git a/packages/components/table/_example/rowspan-colspan/index.wxss b/packages/components/table/_example/rowspan-colspan/index.wxss new file mode 100644 index 000000000..8fae2cf7c --- /dev/null +++ b/packages/components/table/_example/rowspan-colspan/index.wxss @@ -0,0 +1 @@ +/* rowspan-colspan demo 无额外样式 */ diff --git a/packages/components/table/_example/scroll/index.js b/packages/components/table/_example/scroll/index.js new file mode 100644 index 000000000..8b6306b8a --- /dev/null +++ b/packages/components/table/_example/scroll/index.js @@ -0,0 +1,41 @@ +import SkylineBehavior from '@behaviors/skyline.js'; + +Component({ + behaviors: [SkylineBehavior], + data: { + columns: [ + { colKey: 'applicant', title: '标题', width: 180 }, + { colKey: 'status', title: '标题', width: 180 }, + { colKey: 'channel', title: '标题', width: 180 }, + { colKey: 'detail.email', title: '标题', width: 180 }, + ], + data: [], + }, + lifetimes: { + attached() { + const data = []; + const total = 5; + for (let i = 0; i < total; i += 1) { + data.push({ + id: i + 1, + index: i + 1, + applicant: ['横向平铺内容不省略', '内容', '内容'][i % 3], + status: ['横向平铺内容不省略', '内容', '内容'][i % 3], + channel: ['横向平铺内容不省略', '内容', '内容'][i % 3], + detail: { + email: ['横向平铺内容不省略', '内容', '内容'][i % 3], + }, + }); + } + this.setData({ data }); + }, + }, + methods: { + handleRowClick(e) { + console.log('[row-click]', e.detail); + }, + handleCellClick(e) { + console.log('[cell-click]', e.detail); + }, + }, +}); diff --git a/packages/components/table/_example/scroll/index.json b/packages/components/table/_example/scroll/index.json new file mode 100644 index 000000000..e3c621db0 --- /dev/null +++ b/packages/components/table/_example/scroll/index.json @@ -0,0 +1,6 @@ +{ + "component": true, + "usingComponents": { + "t-table": "tdesign-miniprogram/table/table" + } +} diff --git a/packages/components/table/_example/scroll/index.wxml b/packages/components/table/_example/scroll/index.wxml new file mode 100644 index 000000000..1551956ac --- /dev/null +++ b/packages/components/table/_example/scroll/index.wxml @@ -0,0 +1,10 @@ + diff --git a/packages/components/table/_example/scroll/index.wxss b/packages/components/table/_example/scroll/index.wxss new file mode 100644 index 000000000..6c14f83f7 --- /dev/null +++ b/packages/components/table/_example/scroll/index.wxss @@ -0,0 +1 @@ +/* scroll demo 无额外样式 */ diff --git a/packages/components/table/_example/stripe/index.js b/packages/components/table/_example/stripe/index.js new file mode 100644 index 000000000..23516b249 --- /dev/null +++ b/packages/components/table/_example/stripe/index.js @@ -0,0 +1,42 @@ +import SkylineBehavior from '@behaviors/skyline.js'; + +Component({ + behaviors: [SkylineBehavior], + data: { + columns: [ + { colKey: 'applicant', title: '标题' }, + { colKey: 'status', title: '标题' }, + { colKey: 'channel', title: '标题' }, + { colKey: 'detail.email', title: '标题', ellipsis: true }, + ], + data: [], + }, + lifetimes: { + attached() { + const data = []; + const total = 20; + for (let i = 0; i < total; i += 1) { + const content = ['内容', '内容', '内容', '内容', '内容'][i % 5]; + data.push({ + id: i + 1, + index: i + 1, + applicant: content, + status: content, + channel: content, + detail: { + email: content, + }, + }); + } + this.setData({ data }); + }, + }, + methods: { + handleRowClick(e) { + console.log('[row-click]', e.detail); + }, + handleCellClick(e) { + console.log('[cell-click]', e.detail); + }, + }, +}); diff --git a/packages/components/table/_example/stripe/index.json b/packages/components/table/_example/stripe/index.json new file mode 100644 index 000000000..e3c621db0 --- /dev/null +++ b/packages/components/table/_example/stripe/index.json @@ -0,0 +1,6 @@ +{ + "component": true, + "usingComponents": { + "t-table": "tdesign-miniprogram/table/table" + } +} diff --git a/packages/components/table/_example/stripe/index.wxml b/packages/components/table/_example/stripe/index.wxml new file mode 100644 index 000000000..2acdfa8c5 --- /dev/null +++ b/packages/components/table/_example/stripe/index.wxml @@ -0,0 +1,12 @@ + diff --git a/packages/components/table/_example/stripe/index.wxss b/packages/components/table/_example/stripe/index.wxss new file mode 100644 index 000000000..dacf7e23a --- /dev/null +++ b/packages/components/table/_example/stripe/index.wxss @@ -0,0 +1 @@ +/* stripe demo 无额外样式 */ diff --git a/packages/components/table/_example/table.json b/packages/components/table/_example/table.json new file mode 100644 index 000000000..0dd71e440 --- /dev/null +++ b/packages/components/table/_example/table.json @@ -0,0 +1,11 @@ +{ + "navigationBarTitleText": "Table", + "navigationBarBackgroundColor": "#fff", + "usingComponents": { + "base": "./base", + "bordered": "./bordered", + "stripe": "./stripe", + "scroll": "./scroll", + "rowspan-colspan": "./rowspan-colspan" + } +} diff --git a/packages/components/table/_example/table.less b/packages/components/table/_example/table.less new file mode 100644 index 000000000..0be2b2b78 --- /dev/null +++ b/packages/components/table/_example/table.less @@ -0,0 +1,3 @@ +page { + background-color: var(--td-bg-color-page); +} diff --git a/packages/components/table/_example/table.ts b/packages/components/table/_example/table.ts new file mode 100644 index 000000000..560d44d43 --- /dev/null +++ b/packages/components/table/_example/table.ts @@ -0,0 +1 @@ +Page({}); diff --git a/packages/components/table/_example/table.wxml b/packages/components/table/_example/table.wxml new file mode 100644 index 000000000..8975fa46d --- /dev/null +++ b/packages/components/table/_example/table.wxml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/components/table/base-table-props.ts b/packages/components/table/base-table-props.ts new file mode 100644 index 000000000..8ef88a4c2 --- /dev/null +++ b/packages/components/table/base-table-props.ts @@ -0,0 +1,87 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { TdBaseTableProps } from './type'; +const props: TdBaseTableProps = { + /** 是否显示表格边框 */ + bordered: { + type: Boolean, + value: false, + }, + /** 单元格数据为空时呈现的内容 */ + cellEmptyContent: { + type: String, + }, + /** 列配置,泛型 T 指表格数据类型 */ + columns: { + type: Array, + value: [], + }, + /** 数据源,泛型 T 指表格数据类型 */ + data: { + type: Array, + value: [], + }, + /** 空表格呈现样式,支持全局配置 `GlobalConfigProvider` */ + empty: { + type: String, + value: '', + }, + /** 固定行(冻结行),示例:[M, N],表示冻结表头 M 行和表尾 N 行。M 和 N 值为 0 时,表示不冻结行 */ + fixedRows: { + type: Array, + }, + /** 表格高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px。如果不是绝对固定表格高度,建议使用 `maxHeight` */ + height: { + type: null, + }, + /** 加载中状态。值为 `true` 会显示默认加载中样式,可以通过 Function 和 插槽 自定义加载状态呈现内容和样式。值为 `false` 则会取消加载状态 */ + loading: { + type: null, + value: undefined, + }, + /** 透传加载组件全部属性 */ + loadingProps: { + type: Object, + }, + /** 表格最大高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px */ + maxHeight: { + type: null, + }, + /** 唯一标识一行数据的字段名,来源于 `data` 中的字段。如果是字段嵌套多层,可以设置形如 `item.a.id` 的方法 */ + rowKey: { + type: String, + value: 'id', + required: true, + }, + /** 是否显示表头 */ + showHeader: { + type: Boolean, + value: true, + }, + /** 是否显示斑马纹 */ + stripe: { + type: Boolean, + value: false, + }, + /** 表格内容的总宽度,注意不是表格可见宽度。主要应用于 `table-layout: auto` 模式下的固定列显示。`tableContentWidth` 内容宽度的值必须大于表格可见宽度 */ + tableContentWidth: { + type: String, + value: '', + }, + /** 表格布局方式 */ + tableLayout: { + type: String, + value: 'fixed', + }, + /** 行内容上下方向对齐 */ + verticalAlign: { + type: String, + value: 'middle', + }, +}; + +export default props; diff --git a/packages/components/table/table.json b/packages/components/table/table.json new file mode 100644 index 000000000..f20e16478 --- /dev/null +++ b/packages/components/table/table.json @@ -0,0 +1,7 @@ +{ + "component": true, + "styleIsolation": "apply-shared", + "usingComponents": { + "t-loading": "../loading/loading" + } +} diff --git a/packages/components/table/table.less b/packages/components/table/table.less new file mode 100644 index 000000000..563fdfbe5 --- /dev/null +++ b/packages/components/table/table.less @@ -0,0 +1,381 @@ +@import '../common/style/base.less'; + +@table-prefix: ~'@{prefix}-table'; + +// 变量 +@table-border-color: var(--td-table-border-color, @component-border); +@table-header-bg-color: var(--td-table-header-bg-color, @bg-color-container); +@table-header-text-color: var(--td-table-header-text-color, @text-color-placeholder); +@table-body-text-color: var(--td-table-body-text-color, @text-color-primary); +@table-stripe-bg-color: var(--td-table-stripe-bg-color, @bg-color-secondarycontainer); +@table-td-padding: var(--td-table-td-padding, 16rpx 24rpx); +@table-th-padding: var(--td-table-th-padding, 16rpx 24rpx); +@table-font-size: var(--td-table-font-size, @font-body-medium); +@table-loading-bg-color: var(--td-table-loading-bg-color, rgba(255, 255, 255, 0.55)); +@table-row-height: var(--td-table-row-height, 41px); +@scrollbar-color: rgba(0, 0, 0, 10%); +@scrollbar-hover-color: rgba(0, 0, 0, 30%); + +// 固定列相关变量 +@table-fixed-column-z-index: 30; +@table-fixed-cell-border-color: var(--td-table-fixed-cell-border-color, @component-border); +@table-fixed-cell-border-width: 4px; +@table-fixed-cell-border-width-light: 2px; +@table-fixed-cell-border: @table-fixed-cell-border-width solid @table-fixed-cell-border-color; +@table-fixed-cell-border-light: @table-fixed-cell-border-width-light solid @table-fixed-cell-border-color; + +.@{table-prefix} { + position: relative; + width: 100%; + font: @table-font-size; + color: @table-body-text-color; + background-color: @bg-color-container; + box-sizing: border-box; + + &__content { + position: relative; + overflow: auto; + + // scrollbar 样式(同步 mobile-vue) + // scroll-view 在 H5 端内部会生成子滚动容器,需要同时匹配自身和后代元素 + &::-webkit-scrollbar, + & ::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + &::-webkit-scrollbar-thumb, + & ::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: @scrollbar-color; + border-radius: 11px; + } + + &::-webkit-scrollbar-thumb:vertical:hover, + &::-webkit-scrollbar-thumb:horizontal:hover, + & ::-webkit-scrollbar-thumb:vertical:hover, + & ::-webkit-scrollbar-thumb:horizontal:hover { + background-color: @scrollbar-hover-color; + } + } + + &__table-elm { + display: flex; + flex-direction: column; + min-width: 100%; + } + + // 表头 + &__header { + position: relative; + z-index: 1; + } + + &__header-tr, + &__tr { + display: flex; + min-width: 100%; + } + + &__th, + &__td { + flex: 1 1 0; + box-sizing: border-box; + overflow: hidden; + min-width: 0; + border-bottom: 1px solid @table-border-color; + } + + &__th { + padding: @table-th-padding; + background-color: @table-header-bg-color; + color: @table-header-text-color; + font-weight: normal; + text-align: left; + } + + &__td { + padding: @table-td-padding; + text-align: left; + } + + &__th-content, + &__td-content { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + // 空数据 + &__empty-row { + display: flex; + width: 100%; + } + + &__empty { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + padding: 48rpx 0; + background-color: @bg-color-container; + color: @text-color-placeholder; + } + + // 表体 + &__body { + position: relative; + background-color: @bg-color-container; + } + + // 表尾 + &__bottom-content { + padding: 16rpx 24rpx; + background-color: @bg-color-container; + border-top: 1px solid @table-border-color; + } + + // 加载中 + &__loading--full { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background-color: @table-loading-bg-color; + z-index: 10; + } + + // 对齐方式 + .@{prefix}-align-left { + text-align: left; + } + + .@{prefix}-align-center { + text-align: center; + } + + .@{prefix}-align-right { + text-align: right; + } + + // 垂直对齐 + &.@{prefix}-vertical-align-top .@{table-prefix}__td { + vertical-align: top; + } + + &.@{prefix}-vertical-align-middle .@{table-prefix}__td { + vertical-align: middle; + } + + &.@{prefix}-vertical-align-bottom .@{table-prefix}__td { + vertical-align: bottom; + } + + // 边框 + &--bordered { + border: 1px solid @table-border-color; + + .@{table-prefix}__th, + .@{table-prefix}__td { + border-right: 1px solid @table-border-color; + + &:last-child { + border-right: none; + } + } + + .@{table-prefix}__th { + border-bottom: 1px solid @table-border-color; + } + } + + // 斑马纹(同步 mobile-vue) + // 注意:与 mobile-vue 不同,小程序/uniapp 中表头和数据行在不同容器中, + // __body 中的 __tr 从 1 开始计数,因此无论是否有固定表头,都用 odd 着色第1行 + &--striped { + // 非 bordered 模式下,去掉行分割线,让斑马纹效果更纯粹 + &:not(.@{table-prefix}--bordered) { + .@{table-prefix}__th, + .@{table-prefix}__td { + border-bottom: none; + } + } + + // 奇数行着色(第1行灰色,第2行白色...) + .@{table-prefix}__tr:nth-child(odd) { + background-color: @table-stripe-bg-color; + + // 固定列单元格也跟随斑马纹背景色 + .@{table-prefix}__cell--fixed-left, + .@{table-prefix}__cell--fixed-right { + background-color: @table-stripe-bg-color; + } + } + } + + // 布局方式 + &--layout-fixed { + .@{table-prefix}__table-elm { + table-layout: fixed; + } + } + + &--layout-auto { + .@{table-prefix}__table-elm { + table-layout: auto; + } + } + + // 合并单元格 + &--rowspan-colspan { + .@{table-prefix}__td-last-row { + border-bottom: none; + } + + .@{table-prefix}__td-first-col { + border-left: none; + } + } + + // 固定表头(同步 mobile-vue:固定表头时 th 背景色为灰色) + &__header--fixed { + position: sticky; + top: 0; + z-index: 33; + + .@{table-prefix}__th { + background-color: @bg-color-secondarycontainer; + } + } + + // 固定行(确保背景不透明,防止滚动时文字穿透) + &__row--fixed-top, + &__row--fixed-bottom { + background-color: @bg-color-container; + + > .@{table-prefix}__td { + background-color: @bg-color-container; + } + + // 固定行中的固定列单元格也需要背景色 + > .@{table-prefix}__cell--fixed-left, + > .@{table-prefix}__cell--fixed-right { + background-color: @bg-color-container; + } + } + + // 斑马纹模式下固定行也需要保持背景色 + &--striped &__row--fixed-top, + &--striped &__row--fixed-bottom { + &:nth-child(odd) > .@{table-prefix}__td { + background-color: @table-stripe-bg-color; + } + + &:nth-child(even) > .@{table-prefix}__td { + background-color: @bg-color-container; + } + + // 固定列单元格也跟随斑马纹背景色 + &:nth-child(odd) > .@{table-prefix}__cell--fixed-left, + &:nth-child(odd) > .@{table-prefix}__cell--fixed-right { + background-color: @table-stripe-bg-color; + } + + &:nth-child(even) > .@{table-prefix}__cell--fixed-left, + &:nth-child(even) > .@{table-prefix}__cell--fixed-right { + background-color: @bg-color-container; + } + } + + // 固定底部行的第一行加上边框 + &__row--fixed-bottom-first > .@{table-prefix}__td { + border-top: 1px solid @table-border-color; + } + + // 冻结表尾行时,最后一行非冻结行去除下边框 + &__row--without-border-bottom > .@{table-prefix}__td { + border-bottom: none; + } + + // 固定列 + &--column-fixed { + .@{table-prefix}__cell--fixed-left, + .@{table-prefix}__cell--fixed-right { + position: sticky; + z-index: @table-fixed-column-z-index; + background-color: @bg-color-container; + } + + .@{table-prefix}__cell--fixed-right { + z-index: @table-fixed-column-z-index + 1; + } + + // 固定表头中的固定列 th 需要灰色背景(覆盖固定列的白色背景,放在 column-fixed 内部确保优先级) + .@{table-prefix}__header--fixed { + .@{table-prefix}__cell--fixed-left, + .@{table-prefix}__cell--fixed-right { + background-color: @bg-color-secondarycontainer; + } + } + + // 右侧固定列始终显示左边框(同步 mobile-vue) + .@{table-prefix}__cell--fixed-right-first { + border-left: @table-fixed-cell-border-light; + } + + // 固定列边界阴影指示器(伪元素) + .@{table-prefix}__cell--fixed-left-last, + .@{table-prefix}__cell--fixed-right-first { + &::after { + position: absolute; + top: 0; + bottom: 0; + content: ''; + transition: box-shadow 0.3s; + pointer-events: none; + z-index: -1; + } + } + + .@{table-prefix}__cell--fixed-left-last::after { + right: 0; + transform: translateX(100%); + } + + .@{table-prefix}__cell--fixed-right-first::after { + left: 0; + transform: translateX(-100%); + } + } + + // 可向左滚动时,左侧固定列显示阴影 + &__content--scrollable-to-left { + .@{table-prefix}__cell--fixed-left-last::after { + border-right: @table-fixed-cell-border-light; + } + } + + // 可向右滚动时,右侧固定列显示阴影 + &__content--scrollable-to-right { + .@{table-prefix}__cell--fixed-right-first::after { + border-left: @table-fixed-cell-border-light; + } + } + + // 有边框模式下的固定列阴影加粗 + &--bordered &__content--scrollable-to-left { + .@{table-prefix}__cell--fixed-left-last::after { + border-right: @table-fixed-cell-border; + } + } + + &--bordered &__content--scrollable-to-right { + .@{table-prefix}__cell--fixed-right-first::after { + border-left: @table-fixed-cell-border; + } + } +} diff --git a/packages/components/table/table.ts b/packages/components/table/table.ts new file mode 100644 index 000000000..3a1eef915 --- /dev/null +++ b/packages/components/table/table.ts @@ -0,0 +1,464 @@ +import { SuperComponent, wxComponent } from '../common/src/index'; +import config from '../common/config'; +import props from './base-table-props'; +import type { BaseTableCol, TableRowData } from './type'; + +const { prefix } = config; +const name = `${prefix}-table`; + +function get(obj: any, path: string) { + if (!obj || !path) return undefined; + const keys = path.split('.'); + let result = obj; + keys.forEach((key) => { + if (result !== undefined && result !== null) { + result = result[key]; + } + }); + return result; +} + +function formatCSSUnit(unit: string | number | undefined) { + if (!unit) return unit; + return Number.isNaN(Number(unit)) ? unit : `${unit}px`; +} + +@wxComponent() +export default class Table extends SuperComponent { + externalClasses = [`${prefix}-class`]; + + options = { + multipleSlots: true, + }; + + properties = props; + + data = { + prefix, + classPrefix: name, + tableClasses: '', + tableContentStyles: '', + tableElementStyles: '', + theadClasses: '', + tbodyClasses: '', + renderData: [] as any[], + isEmpty: false, + colStyles: [] as string[], + thClassNames: [] as string[], + tdClassNames: [] as string[][], + hasFixedColumn: false, + scrollableToLeft: false, + scrollableToRight: false, + contentClasses: '', + }; + + observers = { + 'bordered, stripe, verticalAlign, loading, rowspanAndColspan, tableLayout, height, maxHeight, tableContentWidth, showHeader, columns, data, cellEmptyContent, rowKey, fixedRows'( + this: any, + ) { + this.updateTable(); + }, + }; + + lifetimes = { + attached(this: any) { + this.updateTable(); + }, + }; + + methods = { + updateTable(this: any) { + const { + bordered, + stripe, + verticalAlign, + loading, + rowspanAndColspan, + tableLayout, + height, + maxHeight, + tableContentWidth, + showHeader, + columns, + data, + cellEmptyContent, + rowKey, + fixedRows, + } = this.properties; + + // 解析 fixedRows + const fixedTopRows = (fixedRows && (fixedRows as number[])[0]) || 0; + const fixedBottomRows = (fixedRows && (fixedRows as number[])[1]) || 0; + + // 是否有固定表头 + const hasFixedHeader = !!(showHeader && (maxHeight || height)); + + // 检测是否有固定列 + const hasFixedColumn = (columns || []).some((col: BaseTableCol) => !!col.fixed); + + // 表格基础类名 + const tableClasses = [ + name, + `${prefix}-vertical-align-${verticalAlign || 'middle'}`, + bordered ? `${name}--bordered` : '', + stripe ? `${name}--striped` : '', + stripe && (maxHeight || height) ? `${name}--header-fixed` : '', + loading ? `${name}--loading` : '', + rowspanAndColspan ? `${name}--rowspan-colspan` : '', + hasFixedColumn ? `${name}--column-fixed` : '', + ] + .filter(Boolean) + .join(' '); + + // 表格内容区域样式 + const contentStyles: string[] = []; + if (height) contentStyles.push(`height: ${formatCSSUnit(height)}`); + if (maxHeight) contentStyles.push(`max-height: ${formatCSSUnit(maxHeight)}`); + const tableContentStyles = contentStyles.join('; '); + + // 表格元素样式 + let tableElementStyles = tableContentWidth ? `width: ${formatCSSUnit(tableContentWidth)}` : ''; + // 有固定列时,自动根据列宽之和计算 tableContentWidth + if (!tableElementStyles && hasFixedColumn && columns && columns.length > 0) { + const totalWidth = (columns as BaseTableCol[]).reduce((sum: number, col: BaseTableCol) => { + return sum + parseFloat(String(col.width || 80)); + }, 0); + tableElementStyles = `width: ${totalWidth}px`; + } + + // 表头类名 + const theadClasses = [`${name}__header`, maxHeight || height ? `${name}__header--fixed` : ''] + .filter(Boolean) + .join(' '); + + const tbodyClasses = `${name}__body`; + + // 表格布局类名 + const layoutClass = `${name}--layout-${tableLayout || 'fixed'}`; + + // 计算固定列偏移量 + const fixedLeftOffsets: number[] = []; + const fixedRightOffsets: number[] = []; + if (hasFixedColumn) { + // 计算左侧固定列的 left 偏移 + let leftOffset = 0; + for (let i = 0; i < (columns || []).length; i += 1) { + fixedLeftOffsets[i] = leftOffset; + const col = columns[i]; + if (col.fixed === 'left') { + const w = parseFloat(String(col.width || 80)); + leftOffset += w; + } + } + // 计算右侧固定列的 right 偏移 + let rightOffset = 0; + for (let i = (columns || []).length - 1; i >= 0; i -= 1) { + fixedRightOffsets[i] = rightOffset; + const col = columns[i]; + if (col.fixed === 'right') { + const w = parseFloat(String(col.width || 80)); + rightOffset += w; + } + } + } + + // 找到最后一个左固定列和第一个右固定列的索引 + let lastFixedLeftIndex = -1; + let firstFixedRightIndex = -1; + (columns || []).forEach((col: BaseTableCol, index: number) => { + if (col.fixed === 'left') lastFixedLeftIndex = index; + if (col.fixed === 'right' && firstFixedRightIndex === -1) firstFixedRightIndex = index; + }); + + // 列样式 + const defaultColWidth = tableLayout === 'fixed' ? '80px' : undefined; + const colStyles = (columns || []).map((col: BaseTableCol, colIndex: number) => { + const width = formatCSSUnit(col.width || defaultColWidth); + const minWidth = + !formatCSSUnit(col.width || defaultColWidth) && !col.minWidth && tableLayout === 'fixed' + ? '80px' + : formatCSSUnit(col.minWidth); + const styles: string[] = []; + // 当列设置了固定宽度时,使用 flex: 0 0 auto 防止被 flex 压缩,支持横向滚动 + if (col.width) { + styles.push('flex: 0 0 auto'); + } + if (width) styles.push(`width: ${width}`); + if (minWidth) styles.push(`min-width: ${minWidth}`); + // 固定列定位 + if (col.fixed === 'left') { + styles.push(`left: ${fixedLeftOffsets[colIndex]}px`); + } else if (col.fixed === 'right') { + styles.push(`right: ${fixedRightOffsets[colIndex]}px`); + } + return styles.join('; '); + }); + + // 表头类名 + const thClassNames = (columns || []).map((col: BaseTableCol, colIndex: number) => { + const classes: string[] = []; + if (col.colKey) classes.push(`${name}__th-${col.colKey}`); + if (col.align && col.align !== 'left') classes.push(`${prefix}-align-${col.align}`); + if (col.fixed === 'left') classes.push(`${name}__cell--fixed-left`); + if (col.fixed === 'right') classes.push(`${name}__cell--fixed-right`); + if (colIndex === lastFixedLeftIndex) classes.push(`${name}__cell--fixed-left-last`); + if (colIndex === firstFixedRightIndex) classes.push(`${name}__cell--fixed-right-first`); + return classes.join(' '); + }); + + // 计算合并单元格 + const skipSpansMap = new Map(); + if (rowspanAndColspan && data?.length && columns?.length) { + for (let i = 0; i < data.length; i += 1) { + const row = data[i]; + for (let j = 0; j < columns.length; j += 1) { + const col = columns[j]; + const cellKey = `${get(row, rowKey || 'id')}_${col.colKey || j}`; + const state = skipSpansMap.get(cellKey) || {}; + const o = (rowspanAndColspan as any)({ row, col, rowIndex: i, colIndex: j }) || {}; + if (o.rowspan || o.colspan || state.rowspan || state.colspan) { + if (o.rowspan) state.rowspan = o.rowspan; + if (o.colspan) state.colspan = o.colspan; + skipSpansMap.set(cellKey, state); + } + // 标记被合并的单元格 + if (state.rowspan || state.colspan) { + const maxRowIndex = i + (state.rowspan || 1); + const maxColIndex = j + (state.colspan || 1); + for (let ri = i; ri < maxRowIndex; ri += 1) { + for (let ci = j; ci < maxColIndex; ci += 1) { + if (ri !== i || ci !== j) { + if (data[ri] && columns[ci]) { + const key = `${get(data[ri], rowKey || 'id')}_${columns[ci].colKey || ci}`; + const s = skipSpansMap.get(key) || {}; + s.skipped = true; + skipSpansMap.set(key, s); + } + } + } + } + } + } + } + } + + // 构建渲染数据 + const renderData = (data || []).map((row: TableRowData, rowIndex: number) => { + const cells = (columns || []).map((col: BaseTableCol, colIndex: number) => { + const cellKey = `${get(row, rowKey || 'id')}_${col.colKey || colIndex}`; + const spanState = skipSpansMap.get(cellKey); + + const tdClasses: string[] = []; + if (col.align && col.align !== 'left') tdClasses.push(`${prefix}-align-${col.align}`); + if (col.fixed === 'left') tdClasses.push(`${name}__cell--fixed-left`); + if (col.fixed === 'right') tdClasses.push(`${name}__cell--fixed-right`); + if (colIndex === lastFixedLeftIndex) tdClasses.push(`${name}__cell--fixed-left-last`); + if (colIndex === firstFixedRightIndex) tdClasses.push(`${name}__cell--fixed-right-first`); + + let cellContent = ''; + if (col.colKey === 'serial-number') { + cellContent = String(rowIndex + 1); + } else if (typeof col.cell === 'function') { + cellContent = col.cell({ row, col, rowIndex, colIndex }); + } else { + const val = get(row, col.colKey || ''); + if (val !== undefined && val !== null && val !== '') { + cellContent = String(val); + } else if (cellEmptyContent) { + cellContent = cellEmptyContent as string; + } + } + + return { + colKey: col.colKey || String(colIndex), + content: cellContent, + className: tdClasses.join(' '), + skipped: spanState?.skipped || false, + rowspan: spanState?.rowspan || 0, + colspan: spanState?.colspan || 0, + isLastRow: !!(spanState?.rowspan && rowIndex + spanState.rowspan === data.length), + isFirstCol: !!(rowspanAndColspan && colIndex === 0), + }; + }); + + return { + rowIndex, + rowId: get(row, rowKey || 'id'), + cells, + row, + rowClass: (() => { + const classes: string[] = []; + const dataLen = data.length; + if (fixedTopRows > 0 && rowIndex < fixedTopRows) { + classes.push(`${name}__row--fixed-top`); + } + if (fixedBottomRows > 0 && rowIndex >= dataLen - fixedBottomRows) { + classes.push(`${name}__row--fixed-bottom`); + if (rowIndex === dataLen - fixedBottomRows) { + classes.push(`${name}__row--fixed-bottom-first`); + } + } + if (fixedBottomRows > 0 && rowIndex === dataLen - fixedBottomRows - 1) { + classes.push(`${name}__row--without-border-bottom`); + } + return classes.join(' '); + })(), + rowStyle: (() => { + const dataLen = data.length; + // 固定行的 z-index 需要高于固定列的 z-index(右侧固定列 z-index 为 31),否则横向滚动时固定列内容会覆盖固定行 + const fixedRowZIndex = this.data.hasFixedColumn ? 32 : 2; + if (fixedTopRows > 0 && rowIndex < fixedTopRows) { + // top/bottom 值将在 afterSetData 中通过实际 DOM 测量来设置 + return `position: sticky; top: 0px; z-index: ${fixedRowZIndex};`; + } + if (fixedBottomRows > 0 && rowIndex >= dataLen - fixedBottomRows) { + return `position: sticky; bottom: 0px; z-index: ${fixedRowZIndex};`; + } + return ''; + })(), + }; + }); + + const isEmpty = !data || data.length === 0; + + // 内容区域类名(滚动阴影) + const contentClasses = [`${name}__content`, hasFixedColumn ? `${name}__content--scrollable-to-right` : ''] + .filter(Boolean) + .join(' '); + + this.setData( + { + tableClasses, + tableContentStyles, + tableElementStyles, + theadClasses, + tbodyClasses, + layoutClass, + renderData, + isEmpty, + colStyles, + thClassNames, + columnsLength: (columns || []).length, + hasFixedColumn, + contentClasses, + }, + () => { + // 动态测量行高,修正固定行的 top/bottom 值 + if (fixedTopRows > 0 || fixedBottomRows > 0) { + this.measureAndFixStickyPositions(hasFixedHeader, fixedTopRows, fixedBottomRows, (data || []).length); + } + }, + ); + }, + + measureAndFixStickyPositions( + this: any, + hasFixedHeader: boolean, + fixedTopRows: number, + fixedBottomRows: number, + dataLen: number, + ) { + const query = this.createSelectorQuery(); + query.select(`.${name}__header`).boundingClientRect(); + query.selectAll(`.${name}__tr`).boundingClientRect(); + query.exec((res: any) => { + if (!res) return; + const headerRect = res[0]; + const rowRects = res[1]; + if (!rowRects || rowRects.length === 0) return; + + const headerHeight = hasFixedHeader && headerRect ? headerRect.height : 0; + const updates: Record = {}; + const fixedRowZIndex = this.data.hasFixedColumn ? 32 : 2; + + // 计算固定顶部行的 top 值 + let topOffset = headerHeight; + for (let i = 0; i < fixedTopRows && i < rowRects.length; i += 1) { + updates[`renderData[${i}].rowStyle`] = `position: sticky; top: ${topOffset}px; z-index: ${fixedRowZIndex};`; + topOffset += rowRects[i].height; + } + + // 计算固定底部行的 bottom 值 + let bottomOffset = 0; + for (let i = dataLen - 1; i >= dataLen - fixedBottomRows && i >= 0; i -= 1) { + if (i < rowRects.length) { + updates[`renderData[${i}].rowStyle`] = + `position: sticky; bottom: ${bottomOffset}px; z-index: ${fixedRowZIndex};`; + bottomOffset += rowRects[i].height; + } + } + + if (Object.keys(updates).length > 0) { + this.setData(updates); + } + }); + }, + + onRowClick(this: any, e: any) { + const { rowIndex } = e.currentTarget.dataset; + const { data } = this.properties; + if (data && data[rowIndex]) { + this.triggerEvent('row-click', { + row: data[rowIndex], + index: rowIndex, + }); + } + }, + + onCellClick(this: any, e: any) { + const { rowIndex, colIndex } = e.currentTarget.dataset; + const { data, columns } = this.properties; + if (data && data[rowIndex] && columns && columns[colIndex]) { + this.triggerEvent('cell-click', { + row: data[rowIndex], + col: columns[colIndex], + rowIndex, + colIndex, + }); + } + }, + + onScroll(this: any, e: any) { + this.triggerEvent('scroll', { e }); + + const target = e.detail || {}; + + // 更新固定列滚动阴影状态 + if (this.data.hasFixedColumn && target.scrollLeft !== undefined) { + const { scrollLeft } = target; + const scrollWidth = target.scrollWidth || 0; + // 使用 createSelectorQuery 获取实际宽度 + this.createSelectorQuery() + .select(`.${name}__content`) + .boundingClientRect() + .exec((res: any) => { + if (res && res[0]) { + const containerWidth = res[0].width; + const canScrollLeft = scrollLeft > 1; + const canScrollRight = scrollWidth - scrollLeft - containerWidth > 1; + const contentClasses = [ + `${name}__content`, + canScrollLeft ? `${name}__content--scrollable-to-left` : '', + canScrollRight ? `${name}__content--scrollable-to-right` : '', + ] + .filter(Boolean) + .join(' '); + + if (contentClasses !== this.data.contentClasses) { + this.setData({ contentClasses }); + } + } + }); + } + + // 滚动到底部检测 + if ( + target.scrollHeight && + target.scrollTop !== undefined && + target.scrollHeight - target.scrollTop - target.clientHeight <= 50 + ) { + this.triggerEvent('scroll-to-bottom'); + } + }, + }; +} diff --git a/packages/components/table/table.wxml b/packages/components/table/table.wxml new file mode 100644 index 000000000..06ddfe88e --- /dev/null +++ b/packages/components/table/table.wxml @@ -0,0 +1,76 @@ + + + + + + + + + + {{col.title}} + + + + + + + + + + {{empty}} + + + + + + + + + {{cell.content}} + + + + + + + + + + + + + + + + {{footerSummary}} + + diff --git a/packages/components/table/type.ts b/packages/components/table/type.ts new file mode 100644 index 000000000..f040c8b83 --- /dev/null +++ b/packages/components/table/type.ts @@ -0,0 +1,188 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import { LoadingProps } from '../loading/index'; +import type { ClassName } from '../common/common'; + +export interface TdBaseTableProps { + /** + * 是否显示表格边框 + * @default false + */ + bordered?: { + type: BooleanConstructor; + value?: boolean; + }; + /** + * 单元格数据为空时呈现的内容 + */ + cellEmptyContent?: { + type: StringConstructor; + value?: string; + }; + /** + * 列配置,泛型 T 指表格数据类型 + * @default [] + */ + columns?: { + type: ArrayConstructor; + value?: Array>; + }; + /** + * 数据源,泛型 T 指表格数据类型 + * @default [] + */ + data?: { + type: ArrayConstructor; + value?: Array; + }; + /** + * 空表格呈现样式,支持全局配置 `GlobalConfigProvider` + * @default '' + */ + empty?: { + type: StringConstructor; + value?: string; + }; + /** + * 固定行(冻结行),示例:[M, N],表示冻结表头 M 行和表尾 N 行。M 和 N 值为 0 时,表示不冻结行 + */ + fixedRows?: { + type: ArrayConstructor; + value?: Array; + }; + /** + * 表格高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px。如果不是绝对固定表格高度,建议使用 `maxHeight` + */ + height?: { + type: null; + value?: string | number; + }; + /** + * 加载中状态。值为 `true` 会显示默认加载中样式,可以通过 Function 和 插槽 自定义加载状态呈现内容和样式。值为 `false` 则会取消加载状态 + */ + loading?: { + type: BooleanConstructor; + value?: boolean; + }; + /** + * 透传加载组件全部属性 + */ + loadingProps?: { + type: ObjectConstructor; + value?: Partial; + }; + /** + * 表格最大高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px + */ + maxHeight?: { + type: null; + value?: string | number; + }; + /** + * 唯一标识一行数据的字段名,来源于 `data` 中的字段。如果是字段嵌套多层,可以设置形如 `item.a.id` 的方法 + * @default 'id' + */ + rowKey: { + type: StringConstructor; + value?: string; + required?: boolean; + }; + /** + * 是否显示表头 + * @default true + */ + showHeader?: { + type: BooleanConstructor; + value?: boolean; + }; + /** + * 是否显示斑马纹 + * @default false + */ + stripe?: { + type: BooleanConstructor; + value?: boolean; + }; + /** + * 表格内容的总宽度,注意不是表格可见宽度。主要应用于 `table-layout: auto` 模式下的固定列显示。`tableContentWidth` 内容宽度的值必须大于表格可见宽度 + * @default '' + */ + tableContentWidth?: { + type: StringConstructor; + value?: string; + }; + /** + * 表格布局方式 + * @default fixed + */ + tableLayout?: { + type: StringConstructor; + value?: 'auto' | 'fixed'; + }; + /** + * 行内容上下方向对齐 + * @default middle + */ + verticalAlign?: { + type: StringConstructor; + value?: 'top' | 'middle' | 'bottom'; + }; +} + +export interface BaseTableCol { + /** + * 列横向对齐方式 + * @default left + */ + align?: 'left' | 'right' | 'center'; + /** + * 自定义单元格渲染。默认使用 `colKey` 的值作为自定义当前列的插槽名称。
如果 `cell` 值类型为 Function 表示以函数形式渲染单元格。值类型为 string 表示使用插槽渲染,插槽名称为 cell 的值。优先级高于 `render`。泛型 T 指表格数据类型 + */ + cell?: string | ((params: BaseTableCellParams) => string); + /** + * 列类名,值类型是 Function 使用返回值作为列类名;值类型不为 Function 时,值用于整列类名(含表头)。泛型 T 指表格数据类型 + */ + className?: TableColumnClassName | TableColumnClassName[]; + /** + * 渲染列所需字段,值为 `serial-number` 表示当前列为「序号」列 + * @default '' + */ + colKey?: string; + /** + * 固定列显示位置 + * @default left + */ + fixed?: 'left' | 'right'; + /** + * 透传 CSS 属性 `min-width` 到 `` 元素。⚠️ 仅少部分浏览器支持,如:使用 [TablesNG](https://docs.google.com/document/d/16PFD1GtMI9Zgwu0jtPaKZJ75Q2wyZ9EZnVbBacOfiNA/preview) 渲染的 Chrome 浏览器支持 `minWidth` + */ + minWidth?: string | number; + /** + * 列宽,可以作为最小宽度使用。当列宽总和小于 `table` 元素时,浏览器根据宽度设置情况自动分配宽度;当列宽总和大于 `table` 元素,表现为定宽。可以同时调整 `table` 元素的宽度来达到自己想要的效果 + */ + width?: string | number; +} + +export interface TableRowData { + [key: string]: any; + children?: TableRowData[]; +} + +export interface BaseTableCellParams { + row: T; + rowIndex: number; + col: BaseTableCol; + colIndex: number; +} + +export type TableColumnClassName = ClassName | ((context: CellData) => ClassName); + +export interface CellData extends BaseTableCellParams { + type: 'th' | 'td'; +} + +export type DataType = TableRowData; diff --git a/packages/tdesign-miniprogram/example/app.json b/packages/tdesign-miniprogram/example/app.json index 1bd9b9e48..eb78aa629 100644 --- a/packages/tdesign-miniprogram/example/app.json +++ b/packages/tdesign-miniprogram/example/app.json @@ -93,6 +93,7 @@ "pages/color-picker/color-picker", "pages/guide/guide", "pages/watermark/watermark", + "pages/table/table", "pages/config-provider/config-provider", "pages/form/form", "pages/chat-message/chat-message" @@ -100,79 +101,127 @@ "subpackages": [ { "root": "pages/chat-list/", - "pages": ["chat-list"] + "pages": [ + "chat-list" + ] }, { "root": "pages/chat-content/", - "pages": ["chat-content"] + "pages": [ + "chat-content" + ] }, { "root": "pages/chat-actionbar/", - "pages": ["chat-actionbar"] + "pages": [ + "chat-actionbar" + ] }, { "root": "pages/chat-loading/", - "pages": ["chat-loading"] + "pages": [ + "chat-loading" + ] }, { "root": "pages/chat-thinking/", - "pages": ["chat-thinking"] + "pages": [ + "chat-thinking" + ] }, { "root": "pages/attachments/", - "pages": ["attachments"] + "pages": [ + "attachments" + ] }, { "root": "pages/chat-markdown/", - "pages": ["chat-markdown"] + "pages": [ + "chat-markdown" + ] }, { "root": "pages/chat-sender/", - "pages": ["chat-sender"] + "pages": [ + "chat-sender" + ] }, { "root": "pages/side-bar/", - "pages": ["side-bar", "base/index", "switch/index", "custom/index", "with-icon/index"] + "pages": [ + "side-bar", + "base/index", + "switch/index", + "custom/index", + "with-icon/index" + ] }, { "root": "pages/action-sheet/", - "pages": ["action-sheet"] + "pages": [ + "action-sheet" + ] }, { "root": "pages/avatar/", - "pages": ["avatar", "skyline/avatar"] + "pages": [ + "avatar", + "skyline/avatar" + ] }, { "root": "pages/calendar/", - "pages": ["calendar"] + "pages": [ + "calendar" + ] }, { "root": "pages/dialog/", - "pages": ["dialog", "skyline/dialog"] + "pages": [ + "dialog", + "skyline/dialog" + ] }, { "root": "pages/picker/", - "pages": ["picker", "skyline/picker"] + "pages": [ + "picker", + "skyline/picker" + ] }, { "root": "pages/rate/", - "pages": ["rate"] + "pages": [ + "rate" + ] }, { "root": "pages/swiper/", - "pages": ["swiper", "skyline/swiper"] + "pages": [ + "swiper", + "skyline/swiper" + ] }, { "root": "pages/swipe-cell/", - "pages": ["swipe-cell"] + "pages": [ + "swipe-cell" + ] }, { "root": "pages/tree-select/", - "pages": ["tree-select"] + "pages": [ + "tree-select" + ] }, { "root": "pages/indexes/", - "pages": ["indexes", "base/index", "custom/index"] + "pages": [ + "indexes", + "base/index", + "custom/index" + ] } ], "themeLocation": "theme.json", @@ -202,4 +251,4 @@ "sdkVersionEnd": "15.255.255" } } -} +} \ No newline at end of file diff --git a/packages/tdesign-miniprogram/example/pages/home/data/display.ts b/packages/tdesign-miniprogram/example/pages/home/data/display.ts index 6681536da..d36dc68e4 100644 --- a/packages/tdesign-miniprogram/example/pages/home/data/display.ts +++ b/packages/tdesign-miniprogram/example/pages/home/data/display.ts @@ -74,6 +74,10 @@ const display = { { name: 'Swiper', label: '轮播图', + }, + { + name: 'Table', + label: '表格', }, { name: 'Tag', diff --git a/packages/tdesign-uniapp/app/pages.json b/packages/tdesign-uniapp/app/pages.json index 25aee7f40..b22219469 100644 --- a/packages/tdesign-uniapp/app/pages.json +++ b/packages/tdesign-uniapp/app/pages.json @@ -174,6 +174,9 @@ { "path": "pages-more/watermark/watermark" }, + { + "path": "pages-more/table/table" + }, { "path": "pages-more/config-provider/config-provider" }, @@ -420,6 +423,10 @@ "name": "watermark", "pathName": "pages-more/watermark/watermark" }, + { + "name": "table", + "pathName": "pages-more/table/table" + }, { "name": "config-provider", "pathName": "pages-more/config-provider/config-provider" diff --git a/packages/tdesign-uniapp/example/src/pages.json b/packages/tdesign-uniapp/example/src/pages.json index fd67adb3f..721c250de 100644 --- a/packages/tdesign-uniapp/example/src/pages.json +++ b/packages/tdesign-uniapp/example/src/pages.json @@ -168,6 +168,9 @@ { "path": "pages-more/swiper/swiper" }, + { + "path": "pages-more/table/table" + }, { "path": "pages-more/tag/tag" }, @@ -554,6 +557,10 @@ "name": "swiper", "pathName": "pages-more/swiper/swiper" }, + { + "name": "table", + "pathName": "pages-more/table/table" + }, { "name": "tag", "pathName": "pages-more/tag/tag" diff --git a/packages/tdesign-uniapp/example/src/pages/home/data/display.json b/packages/tdesign-uniapp/example/src/pages/home/data/display.json index b3dc64563..80412d06e 100644 --- a/packages/tdesign-uniapp/example/src/pages/home/data/display.json +++ b/packages/tdesign-uniapp/example/src/pages/home/data/display.json @@ -68,6 +68,10 @@ "name": "Swiper", "label": "轮播图" }, + { + "name": "Table", + "label": "表格" + }, { "name": "Tag", "label": "标签" @@ -136,4 +140,4 @@ } ] } -} +} \ No newline at end of file diff --git a/packages/tdesign-uniapp/site/docs.config.js b/packages/tdesign-uniapp/site/docs.config.js index 2c728040f..0fa88d99c 100644 --- a/packages/tdesign-uniapp/site/docs.config.js +++ b/packages/tdesign-uniapp/site/docs.config.js @@ -550,6 +550,15 @@ export const docs = [ component: () => import('@/swiper/README.md'), componentEn: () => import('@/swiper/README.en-US.md'), }, + { + title: 'Table 表格', + titleEn: 'Table', + name: 'table', + meta: { docType: 'data' }, + path: '/uniapp/components/table', + component: () => import('@/table/README.md'), + componentEn: () => import('@/table/README.en-US.md'), + }, { title: 'Tag 标签', titleEn: 'Tag', @@ -568,6 +577,7 @@ export const docs = [ component: () => import('@/watermark/README.md'), componentEn: () => import('@/watermark/README.en-US.md'), }, + ], }, { diff --git a/packages/uniapp-components/table/README.en-US.md b/packages/uniapp-components/table/README.en-US.md new file mode 100644 index 000000000..ef642a51c --- /dev/null +++ b/packages/uniapp-components/table/README.en-US.md @@ -0,0 +1,52 @@ +:: BASE_DOC :: + +## API + +### BaseTable Props + +name | type | default | description | required +-- | -- | -- | -- | -- +custom-style | Object | - | CSS(Cascading Style Sheets) | N +bordered | Boolean | false | show table bordered | N +cell-empty-content | String | - | \- | N +columns | Array | [] | table column configs。Typescript: `Array>` | N +data | Array | [] | table data。Typescript: `Array` | N +empty | String | '' | empty text or empty element | N +fixed-rows | Array | - | Typescript: `Array` | N +height | String / Number | - | table height | N +loading | Boolean | undefined | loading state table | N +loading-props | Object | - | Typescript: `Partial`,[Loading API Documents](./loading?tab=api)。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/table/type.ts) | N +max-height | String / Number | - | table max height | N +row-key | String | 'id' | required。unique key for each row data | Y +show-header | Boolean | true | show table header | N +stripe | Boolean | false | show stripe style | N +table-content-width | String | - | \- | N +table-layout | String | fixed | table-layout css properties, [MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/table-layout). set value to be `fixed` on `resizable=true` please。options: auto/fixed | N +vertical-align | String | middle | vertical align。options: top/middle/bottom | N + +### BaseTable Events + +name | params | description +-- | -- | -- +cell-click | `(context: BaseTableCellEventContext)` | trigger on cell clicked。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/table/type.ts)。
`interface BaseTableCellEventContext { row: T; col: BaseTableCol; rowIndex: number; colIndex: number; e: MouseEvent }`
+row-click | `(context: RowEventContext)` | trigger on row click。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/table/type.ts)。
`interface RowEventContext { row: T; index: number; e: MouseEvent }`
+ +### BaseTable Slots + +name | Description +-- | -- +cell-empty-content | \- +empty | empty text or empty element +loading | loading state table + +### BaseTableCol + +name | type | default | description | required +-- | -- | -- | -- | -- +align | String | left | align type。options: left/right/center | N +cell | String / Function | - | use cell to render table cell。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/table/type.ts) | N +class-name | String / Object / Array / Function | - | cell classnames。Typescript: `TableColumnClassName \| TableColumnClassName[]` `type TableColumnClassName = ClassName \| ((context: CellData) => ClassName)` `interface CellData extends BaseTableCellParams { type: 'th' \| 'td' }`。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/common/common.ts)。[see more ts definition](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/table/type.ts) | N +col-key | String | - | unique key for column | N +fixed | String | left | fixed current column to left or right。options: left/right | N +min-width | String / Number | - | add CSS property `min-width` to HTML Element ``,Browsers with [TablesNG](https://docs.google.com/document/d/16PFD1GtMI9Zgwu0jtPaKZJ75Q2wyZ9EZnVbBacOfiNA/preview) support `minWidth` | N +width | String / Number | - | column width | N diff --git a/packages/uniapp-components/table/README.md b/packages/uniapp-components/table/README.md new file mode 100644 index 000000000..8cace55f8 --- /dev/null +++ b/packages/uniapp-components/table/README.md @@ -0,0 +1,52 @@ +:: BASE_DOC :: + +## API + +### BaseTable Props + +名称 | 类型 | 默认值 | 描述 | 必传 +-- | -- | -- | -- | -- +custom-style | Object | - | 自定义样式 | N +bordered | Boolean | false | 是否显示表格边框 | N +cell-empty-content | String | - | 单元格数据为空时呈现的内容 | N +columns | Array | [] | 列配置,泛型 T 指表格数据类型。TS 类型:`Array>` | N +data | Array | [] | 数据源,泛型 T 指表格数据类型。TS 类型:`Array` | N +empty | String | '' | 空表格呈现样式,支持全局配置 `GlobalConfigProvider` | N +fixed-rows | Array | - | 固定行(冻结行),示例:[M, N],表示冻结表头 M 行和表尾 N 行。M 和 N 值为 0 时,表示不冻结行。TS 类型:`Array` | N +height | String / Number | - | 表格高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px。如果不是绝对固定表格高度,建议使用 `maxHeight` | N +loading | Boolean | undefined | 加载中状态。值为 `true` 会显示默认加载中样式,可以通过 Function 和 插槽 自定义加载状态呈现内容和样式。值为 `false` 则会取消加载状态 | N +loading-props | Object | - | 透传加载组件全部属性。TS 类型:`Partial`,[Loading API Documents](./loading?tab=api)。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/table/type.ts) | N +max-height | String / Number | - | 表格最大高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px | N +row-key | String | 'id' | 必需。唯一标识一行数据的字段名,来源于 `data` 中的字段。如果是字段嵌套多层,可以设置形如 `item.a.id` 的方法 | Y +show-header | Boolean | true | 是否显示表头 | N +stripe | Boolean | false | 是否显示斑马纹 | N +table-content-width | String | - | 表格内容的总宽度,注意不是表格可见宽度。主要应用于 `table-layout: auto` 模式下的固定列显示。`tableContentWidth` 内容宽度的值必须大于表格可见宽度 | N +table-layout | String | fixed | 表格布局方式。可选项:auto/fixed | N +vertical-align | String | middle | 行内容上下方向对齐。可选项:top/middle/bottom | N + +### BaseTable Events + +名称 | 参数 | 描述 +-- | -- | -- +cell-click | `(context: BaseTableCellEventContext)` | 单元格点击时触发。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/table/type.ts)。
`interface BaseTableCellEventContext { row: T; col: BaseTableCol; rowIndex: number; colIndex: number; e: MouseEvent }`
+row-click | `(context: RowEventContext)` | 行点击时触发,泛型 T 指表格数据类型。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/table/type.ts)。
`interface RowEventContext { row: T; index: number; e: MouseEvent }`
+ +### BaseTable Slots + +名称 | 描述 +-- | -- +cell-empty-content | 自定义 `cell-empty-content` 显示内容 +empty | 自定义 `empty` 显示内容 +loading | 自定义 `loading` 显示内容 + +### BaseTableCol + +名称 | 类型 | 默认值 | 描述 | 必传 +-- | -- | -- | -- | -- +align | String | left | 列横向对齐方式。可选项:left/right/center | N +cell | String / Function | - | 自定义单元格渲染。默认使用 `colKey` 的值作为自定义当前列的插槽名称。
如果 `cell` 值类型为 Function 表示以函数形式渲染单元格。值类型为 string 表示使用插槽渲染,插槽名称为 cell 的值。优先级高于 `render`。泛型 T 指表格数据类型。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/table/type.ts) | N +class-name | String / Object / Array / Function | - | 列类名,值类型是 Function 使用返回值作为列类名;值类型不为 Function 时,值用于整列类名(含表头)。泛型 T 指表格数据类型。TS 类型:`TableColumnClassName \| TableColumnClassName[]` `type TableColumnClassName = ClassName \| ((context: CellData) => ClassName)` `interface CellData extends BaseTableCellParams { type: 'th' \| 'td' }`。[通用类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/common/common.ts)。[详细类型定义](https://github.com/tencent/tdesign-miniprogram/blob/develop/packages/uniapp-components/table/type.ts) | N +col-key | String | - | 渲染列所需字段,值为 `serial-number` 表示当前列为「序号」列 | N +fixed | String | left | 固定列显示位置。可选项:left/right | N +min-width | String / Number | - | 透传 CSS 属性 `min-width` 到 `` 元素。⚠️ 仅少部分浏览器支持,如:使用 [TablesNG](https://docs.google.com/document/d/16PFD1GtMI9Zgwu0jtPaKZJ75Q2wyZ9EZnVbBacOfiNA/preview) 渲染的 Chrome 浏览器支持 `minWidth` | N +width | String / Number | - | 列宽,可以作为最小宽度使用。当列宽总和小于 `table` 元素时,浏览器根据宽度设置情况自动分配宽度;当列宽总和大于 `table` 元素,表现为定宽。可以同时调整 `table` 元素的宽度来达到自己想要的效果 | N diff --git a/packages/uniapp-components/table/_example/base/index.vue b/packages/uniapp-components/table/_example/base/index.vue new file mode 100644 index 000000000..7486bdc4a --- /dev/null +++ b/packages/uniapp-components/table/_example/base/index.vue @@ -0,0 +1,58 @@ + + + diff --git a/packages/uniapp-components/table/_example/bordered/index.vue b/packages/uniapp-components/table/_example/bordered/index.vue new file mode 100644 index 000000000..609719c83 --- /dev/null +++ b/packages/uniapp-components/table/_example/bordered/index.vue @@ -0,0 +1,59 @@ + + + diff --git a/packages/uniapp-components/table/_example/rowspan-colspan/index.vue b/packages/uniapp-components/table/_example/rowspan-colspan/index.vue new file mode 100644 index 000000000..cac78131e --- /dev/null +++ b/packages/uniapp-components/table/_example/rowspan-colspan/index.vue @@ -0,0 +1,65 @@ + + + diff --git a/packages/uniapp-components/table/_example/scroll/index.vue b/packages/uniapp-components/table/_example/scroll/index.vue new file mode 100644 index 000000000..0ab8ed334 --- /dev/null +++ b/packages/uniapp-components/table/_example/scroll/index.vue @@ -0,0 +1,59 @@ + + + diff --git a/packages/uniapp-components/table/_example/stripe/index.vue b/packages/uniapp-components/table/_example/stripe/index.vue new file mode 100644 index 000000000..667c569d0 --- /dev/null +++ b/packages/uniapp-components/table/_example/stripe/index.vue @@ -0,0 +1,73 @@ + + + diff --git a/packages/uniapp-components/table/_example/table.less b/packages/uniapp-components/table/_example/table.less new file mode 100644 index 000000000..0be2b2b78 --- /dev/null +++ b/packages/uniapp-components/table/_example/table.less @@ -0,0 +1,3 @@ +page { + background-color: var(--td-bg-color-page); +} diff --git a/packages/uniapp-components/table/_example/table.vue b/packages/uniapp-components/table/_example/table.vue new file mode 100644 index 000000000..abbbd70c4 --- /dev/null +++ b/packages/uniapp-components/table/_example/table.vue @@ -0,0 +1,71 @@ + + + + diff --git a/packages/uniapp-components/table/base-table-props.ts b/packages/uniapp-components/table/base-table-props.ts new file mode 100644 index 000000000..fdcd7c78d --- /dev/null +++ b/packages/uniapp-components/table/base-table-props.ts @@ -0,0 +1,103 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import type { TdBaseTableProps } from '../table/type'; +export default { + rowspanAndColspan: { + type: Function, + }, + footerSummary: { + type: String, + }, + /** 是否显示表格边框 */ + bordered: Boolean, + /** 单元格数据为空时呈现的内容 */ + cellEmptyContent: { + type: String, + }, + /** 列配置,泛型 T 指表格数据类型 */ + columns: { + type: Array, + default: (): TdBaseTableProps['columns'] => [], + }, + /** 数据源,泛型 T 指表格数据类型 */ + data: { + type: Array, + default: (): TdBaseTableProps['data'] => [], + }, + /** 空表格呈现样式,支持全局配置 `GlobalConfigProvider` */ + empty: { + type: String, + default: '' as TdBaseTableProps['empty'], + }, + /** 固定行(冻结行),示例:[M, N],表示冻结表头 M 行和表尾 N 行。M 和 N 值为 0 时,表示不冻结行 */ + fixedRows: { + type: Array, + }, + /** 表格高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px。如果不是绝对固定表格高度,建议使用 `maxHeight` */ + height: { + type: [String, Number], + }, + /** 加载中状态。值为 `true` 会显示默认加载中样式,可以通过 Function 和 插槽 自定义加载状态呈现内容和样式。值为 `false` 则会取消加载状态 */ + loading: { + type: [Boolean, null], + default: null as TdBaseTableProps['loading'], + }, + /** 透传加载组件全部属性 */ + loadingProps: { + type: Object, + }, + /** 表格最大高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px */ + maxHeight: { + type: [String, Number], + }, + /** 唯一标识一行数据的字段名,来源于 `data` 中的字段。如果是字段嵌套多层,可以设置形如 `item.a.id` 的方法 */ + rowKey: { + type: String, + default: 'id', + required: true, + }, + /** 是否显示表头 */ + showHeader: { + type: Boolean, + default: true, + }, + /** 是否显示斑马纹 */ + stripe: Boolean, + /** 表格内容的总宽度,注意不是表格可见宽度。主要应用于 `table-layout: auto` 模式下的固定列显示。`tableContentWidth` 内容宽度的值必须大于表格可见宽度 */ + tableContentWidth: { + type: String, + default: '', + }, + /** 表格布局方式 */ + tableLayout: { + type: String, + default: 'fixed' as TdBaseTableProps['tableLayout'], + validator(val: TdBaseTableProps['tableLayout']): boolean { + if (!val) return true; + return ['auto', 'fixed'].includes(val); + }, + }, + /** 行内容上下方向对齐 */ + verticalAlign: { + type: String, + default: 'middle' as TdBaseTableProps['verticalAlign'], + validator(val: TdBaseTableProps['verticalAlign']): boolean { + if (!val) return true; + return ['top', 'middle', 'bottom'].includes(val); + }, + }, + /** 单元格点击时触发 */ + onCellClick: { + type: Function, + default: () => ({}), + }, + /** 行点击时触发,泛型 T 指表格数据类型 */ + onRowClick: { + type: Function, + default: () => ({}), + }, +}; diff --git a/packages/uniapp-components/table/props.ts b/packages/uniapp-components/table/props.ts new file mode 100644 index 000000000..0dc77bfb6 --- /dev/null +++ b/packages/uniapp-components/table/props.ts @@ -0,0 +1,94 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import type { TdBaseTableProps } from '../table/type'; +export default { + /** 是否显示表格边框 */ + bordered: Boolean, + /** 单元格数据为空时呈现的内容 */ + cellEmptyContent: { + type: String, + }, + /** 列配置,泛型 T 指表格数据类型 */ + columns: { + type: Array, + default: (): TdBaseTableProps['columns'] => [], + }, + /** 数据源,泛型 T 指表格数据类型 */ + data: { + type: Array, + default: (): TdBaseTableProps['data'] => [], + }, + /** 空表格呈现样式,支持全局配置 `GlobalConfigProvider` */ + empty: { + type: String, + default: '' as TdBaseTableProps['empty'], + }, + /** 固定行(冻结行),示例:[M, N],表示冻结表头 M 行和表尾 N 行。M 和 N 值为 0 时,表示不冻结行 */ + fixedRows: { + type: Array, + }, + /** 表尾总结行 */ + footerSummary: { + type: String, + }, + /** 表格高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px。如果不是绝对固定表格高度,建议使用 `maxHeight` */ + height: { + type: [String, Number], + }, + /** 加载中状态。值为 `true` 会显示默认加载中样式,可以通过 Function 和 插槽 自定义加载状态呈现内容和样式。值为 `false` 则会取消加载状态 */ + loading: { + type: [Boolean, null], + default: null as TdBaseTableProps['loading'], + }, + /** 透传加载组件全部属性 */ + loadingProps: { + type: Object, + }, + /** 表格最大高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px */ + maxHeight: { + type: [String, Number], + }, + /** 唯一标识一行数据的字段名,来源于 `data` 中的字段。如果是字段嵌套多层,可以设置形如 `item.a.id` 的方法 */ + rowKey: { + type: String, + default: 'id', + }, + /** 用于自定义合并单元格,泛型 T 指表格数据类型。示例:`({ row, col, rowIndex, colIndex }) => { rowspan: 2, colspan: 3 }` */ + rowspanAndColspan: { + type: Function, + }, + /** 是否显示表头 */ + showHeader: { + type: Boolean, + default: true, + }, + /** 是否显示斑马纹 */ + stripe: Boolean, + /** 表格内容的总宽度,注意不是表格可见宽度。主要应用于 `table-layout: auto` 模式下的固定列显示。`tableContentWidth` 内容宽度的值必须大于表格可见宽度 */ + tableContentWidth: { + type: String, + default: '', + }, + /** 表格布局方式 */ + tableLayout: { + type: String, + default: 'fixed' as TdBaseTableProps['tableLayout'], + validator(val: TdBaseTableProps['tableLayout']): boolean { + if (!val) return true; + return ['auto', 'fixed'].includes(val); + }, + }, + /** 行内容上下方向对齐 */ + verticalAlign: { + type: String, + default: 'middle' as TdBaseTableProps['verticalAlign'], + validator(val: TdBaseTableProps['verticalAlign']): boolean { + if (!val) return true; + return ['top', 'middle', 'bottom'].includes(val); + }, + }, +}; diff --git a/packages/uniapp-components/table/table.less b/packages/uniapp-components/table/table.less new file mode 100644 index 000000000..a3c62f740 --- /dev/null +++ b/packages/uniapp-components/table/table.less @@ -0,0 +1,381 @@ +@import '../common/style/base.less'; + +@table-prefix: ~'@{prefix}-table'; + +// 变量 +@table-border-color: var(--td-table-border-color, @component-border); +@table-header-bg-color: var(--td-table-header-bg-color, @bg-color-container); +@table-header-text-color: var(--td-table-header-text-color, @text-color-placeholder); +@table-body-text-color: var(--td-table-body-text-color, @text-color-primary); +@table-stripe-bg-color: var(--td-table-stripe-bg-color, @bg-color-secondarycontainer); +@table-td-padding: var(--td-table-td-padding, 16rpx 24rpx); +@table-th-padding: var(--td-table-th-padding, 16rpx 24rpx); +@table-font-size: var(--td-table-font-size, @font-body-medium); +@table-loading-bg-color: var(--td-table-loading-bg-color, rgba(255, 255, 255, 0.55)); +@table-row-height: var(--td-table-row-height, 41px); +@scrollbar-color: rgba(0, 0, 0, 10%); +@scrollbar-hover-color: rgba(0, 0, 0, 30%); + +// 固定列相关变量 +@table-fixed-column-z-index: 30; +@table-fixed-cell-border-color: var(--td-table-fixed-cell-border-color, @component-border); +@table-fixed-cell-border-width: 4px; +@table-fixed-cell-border-width-light: 2px; +@table-fixed-cell-border: @table-fixed-cell-border-width solid @table-fixed-cell-border-color; +@table-fixed-cell-border-light: @table-fixed-cell-border-width-light solid @table-fixed-cell-border-color; + +.@{table-prefix} { + position: relative; + width: 100%; + font: @table-font-size; + color: @table-body-text-color; + background-color: @bg-color-container; + box-sizing: border-box; + + &__content { + position: relative; + overflow: auto; + + // scrollbar 样式(同步 mobile-vue) + // scroll-view 在 H5 端内部会生成子滚动容器,需要同时匹配自身和后代元素 + &::-webkit-scrollbar, + & ::-webkit-scrollbar { + width: 6px; + height: 6px; + } + + &::-webkit-scrollbar-thumb, + & ::-webkit-scrollbar-thumb { + background-clip: content-box; + background-color: @scrollbar-color; + border-radius: 11px; + } + + &::-webkit-scrollbar-thumb:vertical:hover, + &::-webkit-scrollbar-thumb:horizontal:hover, + & ::-webkit-scrollbar-thumb:vertical:hover, + & ::-webkit-scrollbar-thumb:horizontal:hover { + background-color: @scrollbar-hover-color; + } + } + + &__table-elm { + display: flex; + flex-direction: column; + min-width: 100%; + } + + // 表头 + &__header { + position: relative; + z-index: 1; + } + + &__header-tr, + &__tr { + display: flex; + min-width: 100%; + } + + &__th, + &__td { + flex: 1 1 0; + box-sizing: border-box; + overflow: hidden; + min-width: 0; + border-bottom: 1px solid @table-border-color; + } + + &__th { + padding: @table-th-padding; + background-color: @table-header-bg-color; + color: @table-header-text-color; + font-weight: normal; + text-align: left; + } + + &__td { + padding: @table-td-padding; + text-align: left; + } + + &__th-content, + &__td-content { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + // 空数据 + &__empty-row { + display: flex; + width: 100%; + } + + &__empty { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + padding: 48rpx 0; + background-color: @bg-color-container; + color: @text-color-placeholder; + } + + // 表体 + &__body { + position: relative; + background-color: @bg-color-container; + } + + // 表尾 + &__bottom-content { + padding: 16rpx 24rpx; + background-color: @bg-color-container; + border-top: 1px solid @table-border-color; + } + + // 加载中 + &__loading--full { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + background-color: @table-loading-bg-color; + z-index: 10; + } + + // 对齐方式 + .@{prefix}-align-left { + text-align: left; + } + + .@{prefix}-align-center { + text-align: center; + } + + .@{prefix}-align-right { + text-align: right; + } + + // 垂直对齐 + &.@{prefix}-vertical-align-top .@{table-prefix}__td { + vertical-align: top; + } + + &.@{prefix}-vertical-align-middle .@{table-prefix}__td { + vertical-align: middle; + } + + &.@{prefix}-vertical-align-bottom .@{table-prefix}__td { + vertical-align: bottom; + } + + // 边框 + &--bordered { + border: 1px solid @table-border-color; + + .@{table-prefix}__th, + .@{table-prefix}__td { + border-right: 1px solid @table-border-color; + + &:last-child { + border-right: none; + } + } + + .@{table-prefix}__th { + border-bottom: 1px solid @table-border-color; + } + } + + // 斑马纹(同步 mobile-vue) + // 注意:与 mobile-vue 不同,小程序/uniapp 中表头和数据行在不同容器中, + // __body 中的 __tr 从 1 开始计数,因此无论是否有固定表头,都用 odd 着色第1行 + &--striped { + // 非 bordered 模式下,去掉行分割线,让斑马纹效果更纯粹 + &:not(.@{table-prefix}--bordered) { + .@{table-prefix}__th, + .@{table-prefix}__td { + border-bottom: none; + } + } + + // 奇数行着色(第1行灰色,第2行白色...) + .@{table-prefix}__tr:nth-child(odd) { + background-color: @table-stripe-bg-color; + + // 固定列单元格也跟随斑马纹背景色 + .@{table-prefix}__cell--fixed-left, + .@{table-prefix}__cell--fixed-right { + background-color: @table-stripe-bg-color; + } + } + } + + // 布局方式 + &--layout-fixed { + .@{table-prefix}__table-elm { + table-layout: fixed; + } + } + + &--layout-auto { + .@{table-prefix}__table-elm { + table-layout: auto; + } + } + + // 合并单元格 + &--rowspan-colspan { + .@{table-prefix}__td-last-row { + border-bottom: none; + } + + .@{table-prefix}__td-first-col { + border-left: none; + } + } + + // 固定表头(同步 mobile-vue:固定表头时 th 背景色为灰色) + &__header--fixed { + position: sticky; + top: 0; + z-index: 33; + + .@{table-prefix}__th { + background-color: @bg-color-secondarycontainer; + } + } + + // 固定行(确保背景不透明,防止滚动时文字穿透) + &__row--fixed-top, + &__row--fixed-bottom { + background-color: @bg-color-container; + + > .@{table-prefix}__td { + background-color: @bg-color-container; + } + + // 固定行中的固定列单元格也需要背景色 + > .@{table-prefix}__cell--fixed-left, + > .@{table-prefix}__cell--fixed-right { + background-color: @bg-color-container; + } + } + + // 斑马纹模式下固定行也需要保持背景色 + &--striped &__row--fixed-top, + &--striped &__row--fixed-bottom { + &:nth-child(odd) > .@{table-prefix}__td { + background-color: @table-stripe-bg-color; + } + + &:nth-child(even) > .@{table-prefix}__td { + background-color: @bg-color-container; + } + + // 固定列单元格也跟随斑马纹背景色 + &:nth-child(odd) > .@{table-prefix}__cell--fixed-left, + &:nth-child(odd) > .@{table-prefix}__cell--fixed-right { + background-color: @table-stripe-bg-color; + } + + &:nth-child(even) > .@{table-prefix}__cell--fixed-left, + &:nth-child(even) > .@{table-prefix}__cell--fixed-right { + background-color: @bg-color-container; + } + } + + // 固定底部行的第一行加上边框 + &__row--fixed-bottom-first > .@{table-prefix}__td { + border-top: 1px solid @table-border-color; + } + + // 冻结表尾行时,最后一行非冻结行去除下边框 + &__row--without-border-bottom > .@{table-prefix}__td { + border-bottom: none; + } + + // 固定列 + &--column-fixed { + .@{table-prefix}__cell--fixed-left, + .@{table-prefix}__cell--fixed-right { + position: sticky; + z-index: @table-fixed-column-z-index; + background-color: @bg-color-container; + } + + .@{table-prefix}__cell--fixed-right { + z-index: @table-fixed-column-z-index + 1; + } + + // 固定表头中的固定列 th 需要灰色背景(覆盖固定列的白色背景,放在 column-fixed 内部确保优先级) + .@{table-prefix}__header--fixed { + .@{table-prefix}__cell--fixed-left, + .@{table-prefix}__cell--fixed-right { + background-color: @bg-color-secondarycontainer; + } + } + + // 右侧固定列始终显示左边框(同步 mobile-vue) + .@{table-prefix}__cell--fixed-right-first { + border-left: @table-fixed-cell-border-light; + } + + // 固定列边界阴影指示器(伪元素) + .@{table-prefix}__cell--fixed-left-last, + .@{table-prefix}__cell--fixed-right-first { + &::after { + position: absolute; + top: 0; + bottom: 0; + content: ''; + transition: box-shadow .3s; + pointer-events: none; + z-index: -1; + } + } + + .@{table-prefix}__cell--fixed-left-last::after { + right: 0; + transform: translateX(100%); + } + + .@{table-prefix}__cell--fixed-right-first::after { + left: 0; + transform: translateX(-100%); + } + } + + // 可向左滚动时,左侧固定列显示阴影 + &__content--scrollable-to-left { + .@{table-prefix}__cell--fixed-left-last::after { + border-right: @table-fixed-cell-border-light; + } + } + + // 可向右滚动时,右侧固定列显示阴影 + &__content--scrollable-to-right { + .@{table-prefix}__cell--fixed-right-first::after { + border-left: @table-fixed-cell-border-light; + } + } + + // 有边框模式下的固定列阴影加粗 + &--bordered &__content--scrollable-to-left { + .@{table-prefix}__cell--fixed-left-last::after { + border-right: @table-fixed-cell-border; + } + } + + &--bordered &__content--scrollable-to-right { + .@{table-prefix}__cell--fixed-right-first::after { + border-left: @table-fixed-cell-border; + } + } +} diff --git a/packages/uniapp-components/table/table.vue b/packages/uniapp-components/table/table.vue new file mode 100644 index 000000000..4454ca195 --- /dev/null +++ b/packages/uniapp-components/table/table.vue @@ -0,0 +1,537 @@ + + + diff --git a/packages/uniapp-components/table/type.ts b/packages/uniapp-components/table/type.ts new file mode 100644 index 000000000..869e53238 --- /dev/null +++ b/packages/uniapp-components/table/type.ts @@ -0,0 +1,162 @@ +/* eslint-disable */ + +/** + * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC + * */ + +import type { TdLoadingProps as LoadingProps } from '../loading/type'; +import type { ClassName } from '../common/common'; + +export interface TdBaseTableProps { + footerSummary?: string; + /** + * 是否显示表格边框 + * @default false + */ + bordered?: boolean; + /** + * 单元格数据为空时呈现的内容 + */ + cellEmptyContent?: string; + /** + * 列配置,泛型 T 指表格数据类型 + * @default [] + */ + columns?: Array>; + /** + * 数据源,泛型 T 指表格数据类型 + * @default [] + */ + data?: Array; + /** + * 空表格呈现样式,支持全局配置 `GlobalConfigProvider` + * @default '' + */ + empty?: string; + /** + * 固定行(冻结行),示例:[M, N],表示冻结表头 M 行和表尾 N 行。M 和 N 值为 0 时,表示不冻结行 + */ + fixedRows?: Array; + /** + * 表格高度,超出后会出现滚动条。示例:100, '30%', '300'。值为数字类型,会自动加上单位 px。如果不是绝对固定表格高度,建议使用 `maxHeight` + */ + height?: string | number; + /** + * 加载中状态。值为 `true` 会显示默认加载中样式,可以通过 Function 和 插槽 自定义加载状态呈现内容和样式。值为 `false` 则会取消加载状态 + */ + loading?: boolean | null; + /** + * 透传加载组件全部属性 + */ + 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; +} + +export interface BaseTableCol { + /** + * 列横向对齐方式 + * @default left + */ + align?: 'left' | 'right' | 'center'; + /** + * 自定义单元格渲染。默认使用 `colKey` 的值作为自定义当前列的插槽名称。
如果 `cell` 值类型为 Function 表示以函数形式渲染单元格。值类型为 string 表示使用插槽渲染,插槽名称为 cell 的值。优先级高于 `render`。泛型 T 指表格数据类型 + */ + cell?: string | ((params: BaseTableCellParams) => string); + /** + * 列类名,值类型是 Function 使用返回值作为列类名;值类型不为 Function 时,值用于整列类名(含表头)。泛型 T 指表格数据类型 + */ + className?: TableColumnClassName | TableColumnClassName[]; + /** + * 渲染列所需字段,值为 `serial-number` 表示当前列为「序号」列 + * @default '' + */ + colKey?: string; + /** + * 固定列显示位置 + * @default left + */ + fixed?: 'left' | 'right'; + /** + * 透传 CSS 属性 `min-width` 到 `` 元素。⚠️ 仅少部分浏览器支持,如:使用 [TablesNG](https://docs.google.com/document/d/16PFD1GtMI9Zgwu0jtPaKZJ75Q2wyZ9EZnVbBacOfiNA/preview) 渲染的 Chrome 浏览器支持 `minWidth` + */ + minWidth?: string | 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 type TableColumnClassName = ClassName | ((context: CellData) => ClassName); + +export interface CellData extends BaseTableCellParams { + type: 'th' | 'td'; +} + +export type DataType = TableRowData;