Răsfoiți Sursa

feat: Table add sticky API (#1157)

* feat: Table add sticky API

* docs: Table add sticky header demo

* docs: update sticky header top

Co-authored-by: shijia.me <[email protected]>
走鹃 3 ani în urmă
părinte
comite
376ac12a8f

+ 140 - 15
content/show/table/index-en-US.md

@@ -904,6 +904,130 @@ function App() {
 render(App);
 ```
 
+The header can be fixed to the top of the page with the `sticky` property. v2.21 version support. When passing `top`, you can control the distance from the scroll container.
+
+<StickyHeaderTable />
+
+```jsx live=false noInline=true dir="column"
+import React, { useState, useMemo } from 'react';
+import { Table, Avatar } from '@douyinfe/semi-ui';
+import { IconMore } from '@douyinfe/semi-icons';
+import * as dateFns from 'date-fns';
+
+const DAY = 24 * 60 * 60 * 1000;
+const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
+
+const columns = [
+    {
+        title: '标题',
+        dataIndex: 'name',
+        fixed: true,
+        width: 250,
+        render: (text, record, index) => {
+            return (
+                <div>
+                    <Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
+                    {text}
+                </div>
+            );
+        },
+        filters: [
+            {
+                text: 'Semi Design 设计稿',
+                value: 'Semi Design 设计稿',
+            },
+            {
+                text: 'Semi Pro 设计稿',
+                value: 'Semi Pro 设计稿',
+            },
+        ],
+        onFilter: (value, record) => record.name.includes(value),
+    },
+    {
+        title: '大小',
+        dataIndex: 'size',
+        width: 200,
+        sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
+        render: (text) => `${text} KB`
+    },
+    {
+        title: '所有者',
+        dataIndex: 'owner',
+        width: 200,
+        render: (text, record, index) => {
+            return (
+                <div>
+                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
+                    {text}
+                </div>
+            );
+        }
+
+    },
+    {
+        title: '更新日期',
+        dataIndex: 'updateTime',
+        width: 200,
+        sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
+        render: (value) => {
+            return dateFns.format(new Date(value), 'yyyy-MM-dd');
+        }
+    },
+    {
+        title: '',
+        dataIndex: 'operate',
+        fixed: 'right',
+        align: 'center',
+        width: 100,
+        render: () => {
+            return <IconMore />;
+        }
+    },
+];
+
+function App() {
+    const [dataSource, setData] = useState([]);
+
+    const scroll = useMemo(() => ({ y: 300, x: 1200 }), []);
+    const rowSelection = useMemo(() => ({
+        onChange: (selectedRowKeys, selectedRows) => {
+            console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
+        },
+        getCheckboxProps: record => ({
+            disabled: record.name === 'Michael James', // Column configuration not to be checked
+            name: record.name,
+        }),
+        fixed: true,
+    }), []);
+
+    const getData = () => {
+        const data = [];
+        for (let i = 0; i < 46; i++) {
+            const isSemiDesign = i % 2 === 0;
+            const randomNumber = (i * 1000) % 199;
+            data.push({
+                key: '' + i,
+                name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi Pro 设计稿${i}.fig`,
+                owner: isSemiDesign ? '姜鹏志' : '郝宣',
+                size: randomNumber,
+                updateTime: new Date().valueOf() + randomNumber * DAY,
+                avatarBg: isSemiDesign ? 'grey' : 'red'
+            });
+        }
+        return data;
+    };
+
+    useEffect(() => {
+        const data = getData();
+        setData(data);
+    }, []);
+
+    return <Table sticky={{ top: 60 }} columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
+}
+
+render(App);
+```
+
 ### Table Header With Sorting and Filtering Function
 
 Filters and sorting controls are integrated inside the table, and users can pass in the sorter display of the sorter open header by passing filters in Column and the filter control display of the onFilter open header.
@@ -4401,6 +4525,7 @@ render(App);
 | scroll                  | Whether the table is scrollable, configure the width or height of the scroll area, see [scroll](#scroll)                  | object                                                                                                          | -          |
 | showHeader              | Does it show the header?                                                                                                  | boolean                                                                                                         | true       |
 | size                    | Table size, will effect the `padding` of the rows                                                                         | "default"\|"middle"\|"small"                                                                                    | "default"  | **1.0.0**                                                         |
+| sticky                  | fixed header                                                                    | boolean \| { top: number }                                                                                    | false  | **2.21.0**                               |
 | title                   | Table Title                                                                                                               | string<br/>\|ReactNode<br/>\|(pageData: RecordType[]) => string\|ReactNode                                            |            |
 | virtualized             | Virtualization settings                                                                                                   | Virtualized                                                                                                 | false      | **0.33.0**                                                 |
 | virtualized.itemSize    | Row height                                                                                                                | number\|(index: number) => number                                                                               | 56         | **0.33.0**                                                 |
@@ -4578,22 +4703,22 @@ type Filter = {
 
 ## scroll
 
-| Parameters               | Instructions                                                                                         | Type           | Default | Version       |
-| ------------------------ | ---------------------------------------------------------------------------------------------------- | -------------- | ------- | ------------- |
-| scrollToFirstRowOnChange | Whether to automatically scroll to the top of the table after paging, sorting, and filtering changes | boolean        | false   | 1.1.0 |
-| x                        | Set the width of the horizontal scroll area, which can be pixel value, percentage, or 'max-content'  | string\|number |         |               |
-| y                        | Set the height of the vertical scroll area, which can be a pixel value                               | number         |         |               |
+| Parameters               | Instructions                                                                                         | Type           | Default | Version |
+|--------------------------|------------------------------------------------------------------------------------------------------|----------------|---------|---------|
+| scrollToFirstRowOnChange | Whether to automatically scroll to the top of the table after paging, sorting, and filtering changes | boolean        | false   | 1.1.0   |
+| x                        | Set the width of the horizontal scroll area, which can be pixel value, percentage, or 'max-content'  | string\|number |         |         |
+| y                        | Set the height of the vertical scroll area, which can be a pixel value                               | number         |         |         |
 
 ## pagination
 
 Page-turning component configuration. Pagination suggests not to use literal value.
 
-| Parameters         | Instructions                                                                                                                                                                                                                                                | Type                                                                                         | Default  | Version             |
-| ------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | -------- | ------------------- |
-| currentPage        | Current page number                                                                                                                                                                                                                                         | number                                                                                       | -        |                     |
-| defaultCurrentPage | Default current page number                                                                                                                                                                                                                                 | number                                                                                       | 1        | **>=1.1.0** |
-| formatPageText     | Page-turning area copywriting custom formatting, pass false to close copywriting display; This item affects the copy display on the left of the page turning area of the form. It is different from the `showTotal` parameter of the`Pagination` component. | boolean\| ({ currentStart: number, currentEnd: number, total: number }) => string\|ReactNode | true     | **0.27.0**   |
-| pageSize           | Number of entries per page                                                                                                                                                                                                                                  | number                                                                                       | 10       |                     |
+| Parameters         | Instructions                                                                                                                                                                                                                                                | Type                                                                                         | Default | Version     |
+|--------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------|---------|-------------|
+| currentPage        | Current page number                                                                                                                                                                                                                                         | number                                                                                       | -       |             |
+| defaultCurrentPage | Default current page number                                                                                                                                                                                                                                 | number                                                                                       | 1       | **>=1.1.0** |
+| formatPageText     | Page-turning area copywriting custom formatting, pass false to close copywriting display; This item affects the copy display on the left of the page turning area of the form. It is different from the `showTotal` parameter of the`Pagination` component. | boolean\| ({ currentStart: number, currentEnd: number, total: number }) => string\|ReactNode | true    | **0.27.0**  |
+| pageSize           | Number of entries per page                                                                                                                                                                                                                                  | number                                                                                       | 10      |             |
 | position           | Location                                                                                                                                                                                                                                                    | 'bottom '\|'top '\|'both'                                                                    | 'bottom' |
 | total              | Total number of entries                                                                                                                                                                                                                                     | number                                                                                       | 0        | **>=0.25.0**        |
 
@@ -4604,7 +4729,7 @@ For other configurations, see [Pagination](/en-US/navigation/pagination#API-Refe
 The parameters of the resizable object type, which mainly include event methods when the table column is scaled. These event methods can return an object that merges with the final column.
 
 | Parameters    | Instructions                                               | Type                                             | Default |
-| ------------- | ---------------------------------------------------------- | ------------------------------------------------ | ------- |
+|---------------|------------------------------------------------------------|--------------------------------------------------|---------|
 | onResize      | Triggers when the table column changes its width           | (column: [Column](#Column)) => [Column](#Column) |         |
 | onResizeStart | Triggers when the table column starts to change the width. | (column: [Column](#Column)) => [Column](#Column) |         |
 | onResizeStop  | Triggers when the table column stops changing the width    | (column: [Column](#Column)) => [Column](#Column) |         |
@@ -4642,9 +4767,9 @@ function Demo() {
 }
 ```
 
-| Parameters           | Instructions                                                                                                                     | Version        |
-| -------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------- |
-| getCurrentPageData() | Returns the data object of the current page: { dataSource: RecordType[], groups: Map<{groupKey: string, recordKeys: Set<string\>}> } | 0.37.0 |
+| Parameters           | Instructions                                                                                                                         | Version |
+|----------------------|--------------------------------------------------------------------------------------------------------------------------------------|---------|
+| getCurrentPageData() | Returns the data object of the current page: { dataSource: RecordType[], groups: Map<{groupKey: string, recordKeys: Set<string\>}> } | 0.37.0  |
 
 ## Accessibility
 

+ 152 - 26
content/show/table/index.md

@@ -903,6 +903,131 @@ function App() {
 render(App);
 ```
 
+通过 `sticky` 属性可以将表头固定在页面顶部。v2.21 版本支持。传入 `top` 时可以控制距离滚动容器的距离。
+
+<StickyHeaderTable />
+
+```jsx live=false noInline=true dir="column"
+import React, { useState, useMemo } from 'react';
+import { Table, Avatar } from '@douyinfe/semi-ui';
+import { IconMore } from '@douyinfe/semi-icons';
+import * as dateFns from 'date-fns';
+
+const DAY = 24 * 60 * 60 * 1000;
+const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
+
+const columns = [
+    {
+        title: '标题',
+        dataIndex: 'name',
+        fixed: true,
+        width: 250,
+        render: (text, record, index) => {
+            return (
+                <div>
+                    <Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
+                    {text}
+                </div>
+            );
+        },
+        filters: [
+            {
+                text: 'Semi Design 设计稿',
+                value: 'Semi Design 设计稿',
+            },
+            {
+                text: 'Semi Pro 设计稿',
+                value: 'Semi Pro 设计稿',
+            },
+        ],
+        onFilter: (value, record) => record.name.includes(value),
+    },
+    {
+        title: '大小',
+        dataIndex: 'size',
+        width: 200,
+        sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
+        render: (text) => `${text} KB`
+    },
+    {
+        title: '所有者',
+        dataIndex: 'owner',
+        width: 200,
+        render: (text, record, index) => {
+            return (
+                <div>
+                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
+                    {text}
+                </div>
+            );
+        }
+
+    },
+    {
+        title: '更新日期',
+        dataIndex: 'updateTime',
+        width: 200,
+        sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
+        render: (value) => {
+            return dateFns.format(new Date(value), 'yyyy-MM-dd');
+        }
+    },
+    {
+        title: '',
+        dataIndex: 'operate',
+        fixed: 'right',
+        align: 'center',
+        width: 100,
+        render: () => {
+            return <IconMore />;
+        }
+    },
+];
+
+function App() {
+    const [dataSource, setData] = useState([]);
+
+    const scroll = useMemo(() => ({ y: 300, x: 1200 }), []);
+    const rowSelection = useMemo(() => ({
+        onChange: (selectedRowKeys, selectedRows) => {
+            console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
+        },
+        getCheckboxProps: record => ({
+            disabled: record.name === 'Michael James', // Column configuration not to be checked
+            name: record.name,
+        }),
+        fixed: true,
+    }), []);
+
+    const getData = () => {
+        const data = [];
+        for (let i = 0; i < 46; i++) {
+            const isSemiDesign = i % 2 === 0;
+            const randomNumber = (i * 1000) % 199;
+            data.push({
+                key: '' + i,
+                name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi Pro 设计稿${i}.fig`,
+                owner: isSemiDesign ? '姜鹏志' : '郝宣',
+                size: randomNumber,
+                updateTime: new Date().valueOf() + randomNumber * DAY,
+                avatarBg: isSemiDesign ? 'grey' : 'red'
+            });
+        }
+        return data;
+    };
+
+    useEffect(() => {
+        const data = getData();
+        setData(data);
+    }, []);
+
+    return <Table sticky={{ top: 60 }} columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
+}
+
+render(App);
+```
+
+
 ### 带排序和过滤功能的表头
 
 表格内部集成了过滤器和排序控件,用户可以通过在 Column 中传入 `filters` 以及 `onFilter` 开启表头的过滤器控件展示,传入 `sorter` 开启表头的排序控件的展示。
@@ -4408,6 +4533,7 @@ render(App);
 | scroll                    | 表格是否可滚动,配置滚动区域的宽或高,详见 [scroll](#scroll)                                                   | object                                                                                                          | -          |
 | showHeader                | 是否显示表头                                                                                                   | boolean                                                                                                         | true       |
 | size                      | 表格尺寸,影响表格行 `padding`                                                                                 | "default"\|"middle"\|"small"                                                                                    | "default"  | **1.0.0**                               |
+| sticky                    | 固定表头                                                                           | boolean \| { top: number }                                                                                    | false  | **2.21.0**                               |
 | title                     | 表格标题                                                                                                       | ReactNode<br/>\|(pageData: RecordType[]) => ReactNode                                                           |            |
 | virtualized               | 虚拟化配置                                                                                                     | Virtualized                                                                                                     | false      | **0.33.0**                              |
 | virtualized.itemSize      | 每行的高度                                                                                                     | number\|(index: number) => number                                                                               | 56         | **0.33.0**                              |
@@ -4576,40 +4702,40 @@ type Filter = {
 
 ## rowSelection
 
-| 属性             | 说明                                                                    | 类型                                                                                                 | 默认值 | 版本       |
-| ---------------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | ------ | ---------- |
-| className        | 所处列样式名                                                            | string                                                                                               |        |            |
-| disabled         | 表头的 `Checkbox` 是否禁用                                              | boolean                                                                                              | false  | **0.32.0** |
-| fixed            | 把选择框列固定在左边                                                    | boolean                                                                                              | false  |            |
-| getCheckboxProps | 选择框的默认属性配置                                                    | (record: RecordType) => object                                                                       |        |            |
-| hidden           | 是否隐藏选择列                                                          | boolean                                                                                              | false  | **0.34.0** |
-| selectedRowKeys  | 指定选中项的 key 数组,需要和 onChange 进行配合                         | string[]                                                                                             |        |            |
-| title            | 自定义列表选择框标题                                                    | string\|ReactNode                                                                                    |        |            |
-| width            | 自定义列表选择框宽度                                                    | string\|number                                                                                       |        |            |
-| onChange         | 选中项发生变化时的回调。第一个参数会保存上次选中的 row keys,即使你做了分页受控或更新了 dataSource [FAQ](#faq)    | (selectedRowKeys: number[]\|string[], selectedRows: RecordType[]) => void                            |        |            |
-| onSelect         | 用户手动点击某行选择框的回调                                            | (record: RecordType, selected: boolean, selectedRows: RecordType[], nativeEvent: MouseEvent) => void |        |            |
-| onSelectAll      | 用户手动点击表头选择框的回调,会选中/取消选中 dataSource 里的所有可选行 | (selected: boolean, selectedRows: RecordType[], changedRows: RecordType[]) => void                   |        |            |
+| 属性             | 说明                                                                                                         | 类型                                                                                                 | 默认值 | 版本       |
+|------------------|------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|--------|------------|
+| className        | 所处列样式名                                                                                                 | string                                                                                               |        |            |
+| disabled         | 表头的 `Checkbox` 是否禁用                                                                                   | boolean                                                                                              | false  | **0.32.0** |
+| fixed            | 把选择框列固定在左边                                                                                         | boolean                                                                                              | false  |            |
+| getCheckboxProps | 选择框的默认属性配置                                                                                         | (record: RecordType) => object                                                                       |        |            |
+| hidden           | 是否隐藏选择列                                                                                               | boolean                                                                                              | false  | **0.34.0** |
+| selectedRowKeys  | 指定选中项的 key 数组,需要和 onChange 进行配合                                                               | string[]                                                                                             |        |            |
+| title            | 自定义列表选择框标题                                                                                         | string\|ReactNode                                                                                    |        |            |
+| width            | 自定义列表选择框宽度                                                                                         | string\|number                                                                                       |        |            |
+| onChange         | 选中项发生变化时的回调。第一个参数会保存上次选中的 row keys,即使你做了分页受控或更新了 dataSource [FAQ](#faq) | (selectedRowKeys: number[]\|string[], selectedRows: RecordType[]) => void                            |        |            |
+| onSelect         | 用户手动点击某行选择框的回调                                                                                 | (record: RecordType, selected: boolean, selectedRows: RecordType[], nativeEvent: MouseEvent) => void |        |            |
+| onSelectAll      | 用户手动点击表头选择框的回调,会选中/取消选中 dataSource 里的所有可选行                                       | (selected: boolean, selectedRows: RecordType[], changedRows: RecordType[]) => void                   |        |            |
 
 ## scroll
 
-| 属性                     | 说明                                                       | 类型           | 默认值 | 版本      |
-| ------------------------ | ---------------------------------------------------------- | -------------- | ------ | --------- |
+| 属性                     | 说明                                                     | 类型           | 默认值 | 版本      |
+|--------------------------|--------------------------------------------------------|----------------|--------|-----------|
 | scrollToFirstRowOnChange | 当分页、排序、筛选变化后是否自动滚动到表格顶部             | boolean        | false  | **1.1.0** |
 | x                        | 设置横向滚动区域的宽,可以为像素值、百分比或 'max-content' | string\|number |        |           |
-| y                        | 设置纵向滚动区域的高,可以为像素值                         | number         |        |           |
+| y                        | 设置纵向滚动区域的高,可以为像素值                        | number         |        |           |
 
 ## pagination
 
 翻页组件配置。`pagination` 建议不要使用字面量写法。
 
-| 属性               | 说明                                                                                                                                         | 类型                                                                                          | 默认值   | 版本         |
-| ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | -------- | ------------ |
-| currentPage        | 当前页码                                                                                                                                     | number                                                                                        | -        |              |
-| defaultCurrentPage | 默认的当前页码                                                                                                                               | number                                                                                        | 1        | **>=1.1.0**  |
+| 属性               | 说明                                                                                                                                    | 类型                                                                                          | 默认值   | 版本         |
+|--------------------|---------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|----------|--------------|
+| currentPage        | 当前页码                                                                                                                                | number                                                                                        | -        |              |
+| defaultCurrentPage | 默认的当前页码                                                                                                                          | number                                                                                        | 1        | **>=1.1.0**  |
 | formatPageText     | 翻页区域文案自定义格式化,传 false 关闭文案显示;该项影响表格翻页区域左侧文案显示,不同于 `Pagination` 组件的 `showTotal` 参数,请注意甄别。 | boolean \| ({ currentStart: number, currentEnd: number, total: number }) => string\|ReactNode | true     | **>=0.27.0** |
-| pageSize           | 每页条数                                                                                                                                     | number                                                                                        | 10       |              |
-| position           | 位置                                                                                                                                         | 'bottom'\|'top'\|'both'                                                                       | 'bottom' |              |
-| total              | 数据总数                                                                                                                                     | number                                                                                        | 0        | **>=0.25.0** |
+| pageSize           | 每页条数                                                                                                                                | number                                                                                        | 10       |              |
+| position           | 位置                                                                                                                                    | 'bottom'\|'top'\|'both'                                                                       | 'bottom' |              |
+| total              | 数据总数                                                                                                                                | number                                                                                        | 0        | **>=0.25.0** |
 
 其他配置详见[Pagination](/zh-CN/navigation/pagination#API参考)
 
@@ -4618,7 +4744,7 @@ type Filter = {
 `resizable` 对象型的参数,主要包括一些表格列伸缩时的事件方法。这些事件方法都可以返回一个对象,该对象会和最终的 column 合并。
 
 | 属性          | 说明                     | 类型                                             | 默认值 |
-| ------------- | ------------------------ | ------------------------------------------------ | ------ |
+|---------------|------------------------|--------------------------------------------------|--------|
 | onResize      | 表格列改变宽度时触发     | (column: [Column](#Column)) => [Column](#Column) |        |
 | onResizeStart | 表格列开始改变宽度时触发 | (column: [Column](#Column)) => [Column](#Column) |        |
 | onResizeStop  | 表格列停止改变宽度时触发 | (column: [Column](#Column)) => [Column](#Column) |        |
@@ -4656,8 +4782,8 @@ function Demo() {
 }
 ```
 
-| 名称                 | 描述                                                                                                          | 版本   |
-| -------------------- | ------------------------------------------------------------------------------------------------------------- | ------ |
+| 名称                 | 描述                                                                                                         | 版本   |
+|----------------------|------------------------------------------------------------------------------------------------------------|--------|
 | getCurrentPageData() | 返回当前页的数据对象:{ dataSource: RecordType[], groups: Map<{groupKey: string, recordKeys: Set<string\>}> } | 0.37.0 |
 
 

+ 9 - 0
packages/semi-foundation/table/table.scss

@@ -64,6 +64,15 @@ $module: #{$prefix}-table;
             border-bottom: $width-table_header_border $border-table_base-borderStyle $color-table_th-border-default;
         }
         scrollbar-base-color: transparent;
+
+        &-sticky {
+            position: sticky;
+            z-index: $z-table_fixed_column + 1;
+
+            .semi-table-thead > .semi-table-row > .semi-table-row-head {
+                background-color: $color-table-bg-default;
+            }
+        }
     }
 
     &-body {

+ 12 - 2
packages/semi-ui/table/HeadTable.tsx

@@ -70,7 +70,8 @@ class HeadTable extends React.PureComponent<HeadTableProps> {
             onDidUpdate,
             showHeader,
             anyColumnFixed,
-            bodyHasScrollBar
+            bodyHasScrollBar,
+            sticky
         } = this.props;
 
         if (!showHeader) {
@@ -95,11 +96,20 @@ class HeadTable extends React.PureComponent<HeadTableProps> {
             <TableHeader {...this.props} columns={columns} components={components} onDidUpdate={onDidUpdate} />
         );
 
+        const headTableCls = classnames(`${prefixCls}-header`, {
+            [`${prefixCls}-header-sticky`]: sticky,
+        });
+
+        const stickyTop = get(sticky, 'top', 0);
+        if (typeof stickyTop === 'number') {
+            headStyle.top = stickyTop;
+        }
+
         return (
             <div
                 key="headTable"
                 style={headStyle}
-                className={`${prefixCls}-header`}
+                className={headTableCls}
                 ref={forwardedRef}
                 onScroll={handleBodyScroll}
             >

+ 7 - 1
packages/semi-ui/table/Table.tsx

@@ -258,12 +258,16 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
             isAnyColumnFixed: (columns: ColumnProps<RecordType>[]) =>
                 some(this.getColumns(columns || this.props.columns, this.props.children), column => Boolean(column.fixed)),
             useFixedHeader: () => {
-                const { scroll } = this.props;
+                const { scroll, sticky } = this.props;
 
                 if (get(scroll, 'y')) {
                     return true;
                 }
 
+                if (sticky) {
+                    return true;
+                }
+
                 return false;
             },
             setHeadWidths: (headWidths: Array<BaseHeadWidth>, index = 0) => {
@@ -1099,6 +1103,7 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
             dataSource,
             bodyHasScrollBar,
             disabledRowKeysSet,
+            sticky
         } = props;
         const selectedRowKeysSet = get(rowSelection, 'selectedRowKeysSet', new Set());
 
@@ -1119,6 +1124,7 @@ class Table<RecordType extends Record<string, any>> extends BaseComponent<Normal
                     onHeaderRow={onHeaderRow}
                     dataSource={dataSource}
                     bodyHasScrollBar={bodyHasScrollBar}
+                    sticky={sticky}
                 />
             ) : null;
 

+ 2 - 1
packages/semi-ui/table/_story/v2/index.js

@@ -9,4 +9,5 @@ export { default as FixedOnHeaderRow } from './FixedOnHeaderRow';
 export { default as RadioRowSelection } from './radioRowSelection';
 export { default as FixedVirtualizedEmpty } from './FixedVirtualizedEmpty';
 export { default as FixedFilter } from './FixedFilter';
-export { default as FixedSorter } from './FixedSorter';
+export { default as FixedSorter } from './FixedSorter';
+export { default as stickyHeaderTable } from './stickyHeader';

+ 3 - 0
packages/semi-ui/table/_story/v2/stickyHeader/index.scss

@@ -0,0 +1,3 @@
+body {
+    height: 150vh;
+}

+ 163 - 0
packages/semi-ui/table/_story/v2/stickyHeader/index.tsx

@@ -0,0 +1,163 @@
+import React, { useState, useMemo, useEffect } from 'react';
+// eslint-disable-next-line semi-design/no-import
+import { Table, Avatar, AvatarGroup } from '@douyinfe/semi-ui';
+import { IconMore } from '@douyinfe/semi-icons';
+import * as dateFns from 'date-fns';
+import './index.scss';
+
+const DAY = 24 * 60 * 60 * 1000;
+const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
+
+const columns = [
+    {
+        title: '标题',
+        dataIndex: 'name',
+        fixed: true,
+        width: 250,
+        render: (text, record, index) => {
+            return (
+                <div>
+                    <Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
+                    {text}
+                </div>
+            );
+        },
+        filters: [
+            {
+                text: 'Semi Design 设计稿',
+                value: 'Semi Design 设计稿',
+            },
+            {
+                text: 'Semi Pro 设计稿',
+                value: 'Semi Pro 设计稿',
+            },
+        ],
+        onFilter: (value, record) => record.name.includes(value),
+    },
+    {
+        title: '大小',
+        dataIndex: 'size',
+        width: 200,
+        sorter: (a, b) => (a.size - b.size > 0 ? 1 : -1),
+        render: text => (
+            <div>
+                <AvatarGroup>
+                    <Avatar color="red" alt="Lisa LeBlanc">
+                        LL
+                    </Avatar>
+                    <Avatar alt="Caroline Xiao">CX</Avatar>
+                    <Avatar color="amber" alt="Rafal Matin">
+                        RM
+                    </Avatar>
+                </AvatarGroup>
+            </div>
+        ),
+    },
+    {
+        title: '所有者',
+        dataIndex: 'owner',
+        // width: 200,
+        render: (text, record, index) => {
+            return (
+                <div>
+                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>
+                        {typeof text === 'string' && text.slice(0, 1)}
+                    </Avatar>
+                    {text}
+                </div>
+            );
+        },
+    },
+    {
+        title: '更新日期',
+        dataIndex: 'updateTime',
+        width: 200,
+        sorter: (a, b) => (a.updateTime - b.updateTime > 0 ? 1 : -1),
+        render: value => {
+            return dateFns.format(new Date(value), 'yyyy-MM-dd');
+        },
+    },
+    {
+        title: '',
+        dataIndex: 'operate',
+        fixed: 'right' as const,
+        align: 'center' as const,
+        width: 100,
+        render: () => {
+            return <IconMore />;
+        },
+    },
+];
+
+App.storyName = '固定表头';
+function App() {
+    const [dataSource, setData] = useState([]);
+
+    const scroll = useMemo(() => ({ x: 1200 }), []);
+    const rowSelection = useMemo(
+        () => ({
+            onChange: (selectedRowKeys, selectedRows) => {
+                console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
+            },
+            getCheckboxProps: record => ({
+                disabled: record.name === 'Michael James', // Table.Column configuration not to be checked
+                name: record.name,
+            }),
+            fixed: true,
+        }),
+        []
+    );
+
+    const getData = () => {
+        const data = [];
+        for (let i = 0; i < 46; i++) {
+            const isSemiDesign = i % 2 === 0;
+            const randomNumber = (i * 1000) % 199;
+            data.push({
+                key: '' + i,
+                name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi Pro 设计稿${i}.fig`,
+                owner: isSemiDesign ? '姜鹏志' : '郝宣',
+                size: randomNumber,
+                updateTime: new Date().valueOf() + randomNumber * DAY,
+                avatarBg: isSemiDesign ? 'grey' : 'red',
+            });
+        }
+        return data;
+    };
+
+    useEffect(() => {
+        const data = getData();
+        setData(data);
+    }, []);
+
+    return (
+        <div style={{ height: '250vh' }}>
+            <div style={{ marginTop: 200 }}>
+                <h4>top = 100</h4>
+                <div>
+                    <Table
+                        columns={columns}
+                        dataSource={dataSource}
+                        rowSelection={rowSelection}
+                        scroll={scroll}
+                        sticky={{ top: 100 }}
+                    />
+                </div>
+            </div>
+            <div>
+                <h4>top = 100 + no fixed column</h4>
+                <div>
+                    <Table dataSource={dataSource} rowSelection={rowSelection} sticky={{ top: 100 }} scroll={scroll}>
+                        <Table.Column title="标题" dataIndex="name" key="name" />
+                        <Table.Column title="大小" dataIndex="size" key="size" />
+                        <Table.Column title="所有者" dataIndex="owner" key="owner" />
+                        <Table.Column title="更新时间" dataIndex="updateTime" key="updateTime" />
+                        <Table.Column title="" dataIndex="operate" key="operate" />
+                    </Table>
+                </div>
+            </div>
+        </div>
+    );
+}
+
+export default App;

+ 5 - 1
packages/semi-ui/table/interface.ts

@@ -70,6 +70,7 @@ export interface TableProps<RecordType extends Record<string, any> = any> extend
     onGroupedRow?: OnGroupedRow<RecordType>;
     onHeaderRow?: OnHeaderRow<RecordType>;
     onRow?: OnRow<RecordType>;
+    sticky?: Sticky;
 }
 
 export interface ColumnProps<RecordType extends Record<string, any> = any> {
@@ -318,4 +319,7 @@ export type BodyScrollPosition = 'both' | 'middle' | 'left' | 'right';
 
 export type TableLocale = Locale['Table'];
 export type Direction = CSSDirection;
-export type IncludeGroupRecord<RecordType> = BaseIncludeGroupRecord<RecordType>;
+export type IncludeGroupRecord<RecordType> = BaseIncludeGroupRecord<RecordType>;
+export type Sticky = boolean | {
+    top?: number;
+}

+ 119 - 0
src/demos/StickyHeaderTable/index.tsx

@@ -0,0 +1,119 @@
+import React, { useState, useMemo, useEffect } from 'react';
+import { Table, Avatar } from '@douyinfe/semi-ui';
+import { IconMore } from '@douyinfe/semi-icons';
+import * as dateFns from 'date-fns';
+
+const DAY = 24 * 60 * 60 * 1000;
+const figmaIconUrl = 'https://lf3-static.bytednsdoc.com/obj/eden-cn/ptlz_zlp/ljhwZthlaukjlkulzlp/figma-icon.png';
+
+const columns = [
+    {
+        title: '标题',
+        dataIndex: 'name',
+        fixed: true,
+        width: 250,
+        render: (text, record, index) => {
+            return (
+                <div>
+                    <Avatar size="small" shape="square" src={figmaIconUrl} style={{ marginRight: 12 }}></Avatar>
+                    {text}
+                </div>
+            );
+        },
+        filters: [
+            {
+                text: 'Semi Design 设计稿',
+                value: 'Semi Design 设计稿',
+            },
+            {
+                text: 'Semi Pro 设计稿',
+                value: 'Semi Pro 设计稿',
+            },
+        ],
+        onFilter: (value, record) => record.name.includes(value),
+    },
+    {
+        title: '大小',
+        dataIndex: 'size',
+        width: 200,
+        sorter: (a, b) => a.size - b.size > 0 ? 1 : -1,
+        render: (text) => `${text} KB`
+    },
+    {
+        title: '所有者',
+        dataIndex: 'owner',
+        width: 200,
+        render: (text, record, index) => {
+            return (
+                <div>
+                    <Avatar size="small" color={record.avatarBg} style={{ marginRight: 4 }}>{typeof text === 'string' && text.slice(0, 1)}</Avatar>
+                    {text}
+                </div>
+            );
+        }
+
+    },
+    {
+        title: '更新日期',
+        dataIndex: 'updateTime',
+        width: 200,
+        sorter: (a, b) => a.updateTime - b.updateTime > 0 ? 1 : -1,
+        render: (value) => {
+            return dateFns.format(new Date(value), 'yyyy-MM-dd');
+        }
+    },
+    {
+        title: '',
+        dataIndex: 'operate',
+        fixed: 'right' as const,
+        align: 'center' as const,
+        width: 100,
+        render: () => {
+            return <IconMore />;
+        }
+    },
+];
+
+/**
+ * sticky 特性依赖滚动容器,可编辑代码区域有 overflow auto 属性,不是合适的滚动容器
+ * 因此单独抽出来一个组件,而不是用代码编辑器组件
+ */
+export default function App() {
+    const [dataSource, setData] = useState<any[]>([]);
+
+    const scroll = useMemo(() => ({ y: 300, x: 1200 }), []);
+    const rowSelection = useMemo(() => ({
+        onChange: (selectedRowKeys, selectedRows) => {
+            console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
+        },
+        getCheckboxProps: record => ({
+            disabled: record.name === 'Michael James', // Column configuration not to be checked
+            name: record.name,
+        }),
+        fixed: true,
+    }), []);
+
+    const getData = () => {
+        const data: any[] = [];
+        for (let i = 0; i < 46; i++) {
+            const isSemiDesign = i % 2 === 0;
+            const randomNumber = (i * 1000) % 199;
+            data.push({
+                key: '' + i,
+                name: isSemiDesign ? `Semi Design 设计稿${i}.fig` : `Semi Pro 设计稿${i}.fig`,
+                owner: isSemiDesign ? '姜鹏志' : '郝宣',
+                size: randomNumber,
+                updateTime: new Date().valueOf() + randomNumber * DAY,
+                avatarBg: isSemiDesign ? 'grey' : 'red'
+            });
+        }
+        return data;
+    };
+
+    useEffect(() => {
+        const data = getData();
+        setData(data);
+    }, []);
+
+    return <Table sticky={{ top: 60 }} columns={columns} dataSource={dataSource} rowSelection={rowSelection} scroll={scroll} />;
+}

+ 3 - 0
src/templates/postTemplate.js

@@ -49,6 +49,8 @@ import transContent, { getAnotherSideUrl, isHaveUedDocs, isJumpToDesignSite } fr
 import ImageBox from 'components/ImageBox';
 import './toUEDUtils/toUED.scss';
 import { debounce } from 'lodash';
+import StickyHeaderTable from '../demos/StickyHeaderTable';
+
 const Text = ({ lang, letterSpacing, size, lineHeight, text }) => {
     letterSpacing = letterSpacing || 'auto';
     return (
@@ -458,6 +460,7 @@ const components = {
         // }
     },
     ApiType,
+    StickyHeaderTable
 };
 
 const getPrevAndNext = pageContext => {