diff --git a/docs/zh-CN/components/table-v2.md b/docs/zh-CN/components/table-v2.md
new file mode 100755
index 000000000..c34090eec
--- /dev/null
+++ b/docs/zh-CN/components/table-v2.md
@@ -0,0 +1,901 @@
+---
+title: Table v2 表格
+description:
+type: 0
+group: ⚙ 组件
+menuName: Table 表格
+icon:
+order: 67
+---
+
+表格展示,不支持配置初始化接口初始化数据域,所以需要搭配类似像`Service`这样的,具有配置接口初始化数据域功能的组件,或者手动进行数据域初始化,然后通过`source`属性,获取数据链中的数据,完成数据展示。
+
+## 基本用法
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=5",
+ "body": [
+ {
+ "type": "table-v2",
+ "title": "表格标题",
+ "source": "$rows",
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine"
+ },
+ {
+ "title": "Version",
+ "key": "version"
+ },
+ {
+ "title": "Browser",
+ "key": "browser"
+ },
+ {
+ "title": "Operation",
+ "key": "operation",
+ "type": "button",
+ "label": "删除",
+ "size": "sm"
+ }
+ ],
+ "footer": "表格Footer"
+ }
+ ]
+}
+```
+
+## 可选择 - 多选
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=5",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "rowSelection": {
+ "type": "checkbox",
+ "keyField": "id"
+ },
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine"
+ },
+ {
+ "title": "Version",
+ "key": "version"
+ },
+ {
+ "title": "Browser",
+ "key": "browser"
+ },
+ {
+ "title": "Operation",
+ "key": "operation",
+ "type": "button",
+ "label": "删除",
+ "size": "sm"
+ }
+ ]
+ }
+ ]
+}
+```
+
+## 可选择 - 单选
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=5",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "rowSelection": {
+ "type": "radio",
+ "keyField": "id",
+ "disableOn": "this.record.id === 1"
+ },
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine"
+ },
+ {
+ "title": "Version",
+ "key": "version"
+ },
+ {
+ "title": "Browser",
+ "key": "browser"
+ },
+ {
+ "title": "Operation",
+ "key": "operation",
+ "type": "button",
+ "label": "删除",
+ "size": "sm"
+ }
+ ]
+ }
+ ]
+}
+```
+
+## 筛选和排序
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=5",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine",
+ "sorter": true,
+ "filterMultiple": true,
+ "filters": [
+ {
+ "text": "Joe",
+ "value": "Joe"
+ },
+ {
+ "text": "Jim",
+ "value": "Jim"
+ }
+ ]
+ },
+ {
+ "title": "Version",
+ "key": "version",
+ "sorter": true,
+ "width": 100
+ },
+ {
+ "title": "Browser",
+ "key": "browser",
+ "filters": [
+ {
+ "text": "Joe",
+ "value": "Joe"
+ },
+ {
+ "text": "Jim",
+ "value": "Jim"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
+```
+
+## 带边框
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=5",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "bordered": true,
+ "title": "标题",
+ "footer": "Footer",
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine"
+ },
+ {
+ "title": "Version",
+ "key": "version"
+ },
+ {
+ "title": "Browser",
+ "key": "browser"
+ },
+ {
+ "title": "Operation",
+ "key": "operation",
+ "type": "button",
+ "label": "删除",
+ "size": "sm"
+ }
+ ]
+ }
+ ]
+}
+```
+
+## 可展开
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=5",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine"
+ },
+ {
+ "title": "Version",
+ "key": "version"
+ },
+ {
+ "title": "Browser",
+ "key": "browser"
+ },
+ {
+ "title": "Operation",
+ "key": "operation",
+ "type": "button",
+ "label": "删除",
+ "size": "sm"
+ }
+ ],
+ "expandable": {
+ "expandableOn": "this.id === 1 || this.id === 3",
+ "keyField": "id",
+ "type": "tpl",
+ "html": "
测试测试
",
+ "expandedRowClassNameExpr": "<%= data.rowIndex % 2 ? 'bg-success' : '' %>",
+ "expandedRowKeys": ["3"]
+ }
+ }
+ ]
+}
+```
+
+## 表格行/列合并
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=10",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine"
+ },
+ {
+ "title": "Version",
+ "key": "version",
+ "rowSpanExpr": "<%= data.rowIndex === 2 ? 2 : 0 %>"
+ },
+ {
+ "title": "Browser",
+ "key": "browser"
+ },
+ {
+ "title": "Badge",
+ "key": "badgeText",
+ "colSpanExpr": "<%= data.rowIndex === 6 ? 3 : 0 %>"
+ },
+ {
+ "title": "Grade",
+ "key": "grade"
+ },
+ {
+ "title": "Platform",
+ "key": "platform"
+ }
+ ]
+ }
+ ]
+}
+```
+
+## 固定表头
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=10",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "scroll": {"y" : 200},
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine"
+ },
+ {
+ "title": "Grade",
+ "key": "grade"
+ },
+ {
+ "title": "Platform",
+ "key": "platform"
+ }
+ ]
+ }
+ ]
+}
+```
+
+## 固定列
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=6",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "scroll": {"x": 1000},
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine",
+ "fixed": "left",
+ "width": 100
+ },
+ {
+ "title": "Version",
+ "key": "version",
+ "fixed": "left",
+ "width": 100
+ },
+ {
+ "title": "Browser",
+ "key": "browser"
+ },
+ {
+ "title": "Badge",
+ "key": "badgeText"
+ },
+ {
+ "title": "Grade",
+ "key": "grade"
+ },
+ {
+ "title": "Platform",
+ "key": "platform",
+ "fixed": "right"
+ }
+ ]
+ }
+ ]
+}
+```
+
+## 固定头和列
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=10",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "scroll": {"x": 1000, "y": 200},
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine",
+ "fixed": "left",
+ "width": 100
+ },
+ {
+ "title": "Version",
+ "key": "version",
+ "fixed": "left",
+ "width": 100
+ },
+ {
+ "title": "Browser",
+ "key": "browser"
+ },
+ {
+ "title": "Badge",
+ "key": "badgeText"
+ },
+ {
+ "title": "Grade",
+ "key": "grade"
+ },
+ {
+ "title": "Platform",
+ "key": "platform",
+ "fixed": "right"
+ }
+ ]
+ }
+ ]
+}
+```
+
+## 表头分组
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=10",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "scroll": {"y": 200},
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine",
+ },
+ {
+ "title": "Version",
+ "key": "version",
+ "fixed": "left"
+ },
+ {
+ "title": "Grade",
+ "key": "grade"
+ },
+ {
+ "title": "Grade1",
+ "key": "grade1",
+ "children": [
+ {
+ "title": "Browser",
+ "key": "browser"
+ },
+ {
+ "title": "Badge",
+ "key": "badgeText",
+ "children": [
+ {
+ "title": "ID",
+ "key": "id"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "title": "Platform",
+ "key": "platform",
+ "fixed": "right"
+ }
+ ]
+ }
+ ]
+}
+```
+
+## 拖拽排序
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=5",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "draggable": true,
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine",
+ },
+ {
+ "title": "Version",
+ "key": "version",
+ "fixed": "left"
+ },
+ {
+ "title": "Grade",
+ "key": "grade"
+ },
+ {
+ "title": "Browser",
+ "key": "browser"
+ },
+ {
+ "title": "Badge",
+ "key": "badgeText",
+ "children": [
+ {
+ "title": "ID",
+ "key": "id"
+ }
+ ]
+ },
+ {
+ "title": "Platform",
+ "key": "platform",
+ "fixed": "right"
+ }
+ ]
+ }
+ ]
+}
+```
+
+## 顶部总结栏
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=10",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "scroll": {"y": 200},
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine",
+ },
+ {
+ "title": "Version",
+ "key": "version"
+ },
+ {
+ "title": "Grade",
+ "key": "grade"
+ },
+ {
+ "title": "Browser",
+ "key": "browser"
+ },
+ {
+ "title": "Badge",
+ "key": "badgeText"
+ },
+ {
+ "title": "Platform",
+ "key": "platform"
+ }
+ ],
+ "headSummary": [
+ {
+ "type": "text",
+ "text": "总计"
+ },
+ {
+ "type": "tpl",
+ "tpl": "测试测试",
+ "colSpan": 5
+ }
+ ],
+ "rowSelection": {
+ "type": "checkbox",
+ "keyField": "id"
+ }
+ }
+ ]
+}
+```
+
+## 尾部总结栏
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=10",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "bordered": true,
+ "scroll": {"y": 200, "x": 1000},
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine",
+ "fixed": "left"
+ },
+ {
+ "title": "Version",
+ "key": "version"
+ },
+ {
+ "title": "Grade",
+ "key": "grade"
+ },
+ {
+ "title": "Browser",
+ "key": "browser"
+ },
+ {
+ "title": "Badge",
+ "key": "badgeText"
+ },
+ {
+ "title": "Platform",
+ "key": "platform"
+ }
+ ],
+ "footSummary": [
+ {
+ "type": "text",
+ "text": "总计",
+ "fixed": 'left'
+ },
+ {
+ "type": "tpl",
+ "tpl": "测试测试",
+ "colSpan": 5
+ }
+ ]
+ }
+ ]
+}
+```
+
+## 调整列宽
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=6",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "bordered": true,
+ "scroll": {"x": 1000},
+ "resizable": true,
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine",
+ "width": 200,
+ "align": "center"
+ },
+ {
+ "title": "Version",
+ "key": "version",
+ "width": 200,
+ "align": "right"
+ },
+ {
+ "title": "Grade",
+ "key": "grade",
+ "width": 200
+ },
+ {
+ "title": "Browser",
+ "key": "browser",
+ "width": 200
+ },
+ {
+ "title": "Badge",
+ "key": "badgeText"
+ },
+ {
+ "title": "Platform",
+ "key": "platform"
+ }
+ ]
+ }
+ ]
+}
+```
+
+## 自定义列
+
+```schema: scope="body"
+{
+ "type": "service",
+ "api": "/api/sample?perPage=6",
+ "body": [
+ {
+ "type": "table-v2",
+ "source": "$rows",
+ "columnsToggable": true,
+ "title": "表格的标题",
+ "bordered": true,
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine",
+ "width": 200
+ },
+ {
+ "title": "Version",
+ "key": "version",
+ "width": 200
+ },
+ {
+ "title": "Browser",
+ "key": "browser",
+ "width": 200,
+ "children": [
+ {
+ "title": "Grade",
+ "key": "grade",
+ "width": 200
+ }
+ ]
+ },
+ {
+ "title": "Badge",
+ "key": "badgeText"
+ },
+ {
+ "title": "Platform",
+ "key": "platform"
+ }
+ ]
+ }
+ ]
+}
+```
+
+## 数据为空
+
+```schema
+{
+ "type": "table-v2",
+ "data": {
+ "items": []
+ },
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine",
+ "width": 200
+ },
+ {
+ "title": "Version",
+ "key": "version",
+ "width": 200
+ },
+ {
+ "title": "Browser",
+ "key": "browser",
+ "width": 200,
+ "children": [
+ {
+ "title": "Grade",
+ "key": "grade",
+ "width": 200
+ }
+ ]
+ },
+ {
+ "title": "Platform",
+ "key": "platform",
+ "children": [
+ {
+ "title": "Badge",
+ "key": "badgeText"
+ }
+ ]
+ }
+ ],
+ "placeholder": "暂无数据"
+}
+```
+
+## 数据为空
+
+```schema
+{
+ "type": "table-v2",
+ "data": {
+ "items": []
+ },
+ "columns": [
+ {
+ "title": "Engine",
+ "key": "engine",
+ "width": 200
+ },
+ {
+ "title": "Version",
+ "key": "version",
+ "width": 200
+ },
+ {
+ "title": "Browser",
+ "key": "browser",
+ "width": 200,
+ "children": [
+ {
+ "title": "Grade",
+ "key": "grade",
+ "width": 200
+ },
+ {
+ "title": "Badge",
+ "key": "badgeText",
+ "children": [
+ {
+ "title": "Platform",
+ "key": "platform"
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "loading": true
+}
+```
+
+## 树形结构
+
+## 列搜索
+
+## 粘性头部
+
+## 表格尺寸
+
+## 单元格自动省略
+
+## 响应式列
+
+## Footable
+
+## 属性表
+
+| 属性名 | 类型 | 默认值 | 说明 |
+| ---------------- | ---------------------------------------- | ------------------------- | ------------------------------------------------------------------------- |
+| type | `string` | | `"type"` 指定为 table 渲染器 |
+| title | `string` | | 标题 |
+| source | `string` | `${items}` | 数据源, 绑定当前环境变量 |
+| affixHeader | `boolean` | `true` | 是否固定表头 |
+| columnsTogglable | `auto` 或者 `boolean` | `auto` | 展示列显示开关, 自动即:列数量大于或等于 5 个时自动开启 |
+| placeholder | string | `暂无数据` | 当没数据的时候的文字提示 |
+| className | `string` | `panel-default` | 外层 CSS 类名 |
+| tableClassName | `string` | `table-db table-striped` | 表格 CSS 类名 |
+| headerClassName | `string` | `Action.md-table-header` | 顶部外层 CSS 类名 |
+| footerClassName | `string` | `Action.md-table-footer` | 底部外层 CSS 类名 |
+| toolbarClassName | `string` | `Action.md-table-toolbar` | 工具栏 CSS 类名 |
+| columns | `Array` | | 用来设置列信息 |
+| combineNum | `number` | | 自动合并单元格 |
+| itemActions | Array<[Action](./action-button)> | | 悬浮行操作按钮组 |
+| itemCheckableOn | [表达式](../../docs/concepts/expression) | | 配置当前行是否可勾选的条件,要用 [表达式](../../docs/concepts/expression) |
+| itemDraggableOn | [表达式](../../docs/concepts/expression) | | 配置当前行是否可拖拽的条件,要用 [表达式](../../docs/concepts/expression) |
+| checkOnItemClick | `boolean` | `false` | 点击数据行是否可以勾选当前行 |
+| rowClassName | `string` | | 给行添加 CSS 类名 |
+| rowClassNameExpr | [模板](../../docs/concepts/template) | | 通过模板给行添加 CSS 类名 |
+| prefixRow | `Array` | | 顶部总结行 |
+| affixRow | `Array` | | 底部总结行 |
+| itemBadge | [`BadgeSchema`](./badge) | | 行角标配置 |
+| autoFillHeight | `boolean` | | 内容区域自适应高度 |
+
+## 列配置属性表
+
+| 属性名 | 类型 | 默认值 | 说明 |
+| ---------- | --------------------------------------------- | ------- | ---------------- |
+| label | [模板](../../docs/concepts/template) | | 表头文本内容 |
+| name | `string` | | 通过名称关联数据 |
+| fixed | `left` \| `right` \| `none` | | 是否固定当前列 |
+| popOver | | | 弹出框 |
+| quickEdit | | | 快速编辑 |
+| copyable | `boolean` 或 `{icon: string, content:string}` | | 是否可复制 |
+| sortable | `boolean` | `false` | 是否可排序 |
+| searchable | `boolean` \| `Schema` | `false` | 是否可快速搜索 |
+| width | `number` \| `string` | 列宽 |
+| remark | | | 提示信息 |
diff --git a/examples/components/Components.tsx b/examples/components/Components.tsx
index 017f0d74d..0afa775e5 100644
--- a/examples/components/Components.tsx
+++ b/examples/components/Components.tsx
@@ -775,6 +775,14 @@ export const components = [
import('../../docs/zh-CN/components/table.md').then(wrapDoc)
)
},
+ {
+ label: 'Table v2 表格',
+ path: '/zh-CN/components/table-v2',
+ getComponent: () =>
+ import('../../docs/zh-CN/components/table-v2.md').then(
+ makeMarkdownRenderer
+ )
+ },
{
label: 'Table View 表格视图',
path: '/zh-CN/components/table-view',
diff --git a/scss/_properties.scss b/scss/_properties.scss
index 5fb177857..add4f3e3d 100644
--- a/scss/_properties.scss
+++ b/scss/_properties.scss
@@ -1326,6 +1326,10 @@
--TableCell-sortBtn--onActive-color: var(--primary);
--TableCell-sortBtn-width: #{px2rem(8px)};
+ --Table-fixedLeftLast-boxShadow: inset 10px 0 8px -8px #00000026;
+ --Table-fixedRightFirst-boxShadow: inset -10px 0 8px -8px #00000026;
+ --Table-loading-padding: 30px 0px;
+
--Tabs--card-bg: #f6f8f8;
--Tabs--card-borderTopColor: var(--borderColor);
--Tabs--card-linkMargin: 0 10px 0 0;
diff --git a/scss/components/_table.scss b/scss/components/_table.scss
index 03a91aabe..7ee3c73ba 100644
--- a/scss/components/_table.scss
+++ b/scss/components/_table.scss
@@ -8,6 +8,34 @@
margin-bottom: var(--gap-sm);
}
+ &-bordered {
+ border: var(--Table-borderWidth) solid var(--Table-borderColor);
+ border-collapse: inherit;
+
+ .#{$ns}Table-container {
+ border-top: var(--Table-borderWidth) solid var(--Table-borderColor);
+ }
+
+ .#{$ns}Table-table {
+ > thead > tr > th,
+ > tbody > tr > td,
+ > tfoot > tr > td {
+ border-right: var(--Table-borderWidth) solid var(--Table-borderColor);
+
+ &:last-child {
+ border-right: none;
+ }
+ }
+ }
+
+ .#{$ns}Table-footer {
+ border-top: var(--Table-borderWidth) solid var(--Table-borderColor);
+ }
+ .#{$ns}Table-title {
+ border-bottom: var(--Table-borderWidth) solid var(--Table-borderColor);
+ }
+ }
+
&-fixedLeft,
&-fixedRight {
position: absolute;
@@ -99,7 +127,9 @@
}
}
- &-heading {
+ &-heading,
+ &-title,
+ &-footer {
background: var(--Table-heading-bg);
padding: calc(
(
@@ -231,7 +261,6 @@
background: var(--Table-bg);
border-spacing: 0;
border-collapse: collapse;
- border: var(--Table-borderWidth) solid var(--Table-borderColor);
& th,
& td {
@@ -603,6 +632,124 @@
);
position: relative;
}
+
+ > tbody > tr > td.#{$ns}Table-cell-fix-left,
+ > tbody > tr > td.#{$ns}Table-cell-fix-right,
+ > tfoot > tr > td.#{$ns}Table-cell-fix-left,
+ > tfoot > tr > td.#{$ns}Table-cell-fix-right {
+ background: #FFF;
+ }
+
+ > tbody > tr > td.#{$ns}Table-cell-row-hover {
+ background: var(--Table-onHover-bg);
+ border-color: var(--Table-onHover-borderColor);
+ color: var(--Table-onHover-color);
+ }
+
+ > thead > tr > th.#{$ns}Table-cell-fix-left-last,
+ > tbody > tr > td.#{$ns}Table-cell-fix-left-last,
+ > tfoot > tr > td.#{$ns}Table-cell-fix-left-last {
+ &:after {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: -1px;
+ width: 30px;
+ transform: translate(100%);
+ transition: box-shadow .3s;
+ content: "";
+ pointer-events: none;
+ }
+ }
+
+ > thead > tr > th.#{$ns}Table-cell-fix-right-first,
+ > tbody > tr > td.#{$ns}Table-cell-fix-right-first,
+ > tfoot > tr > td.#{$ns}Table-cell-fix-right-last {
+ &:after {
+ position: absolute;
+ top: 0;
+ bottom: -1px;
+ left: 0;
+ width: 30px;
+ transform: translate(-100%);
+ transition: box-shadow .3s;
+ content: "";
+ pointer-events: none;
+ }
+ }
+
+ > tbody > tr > td.#{$ns}Table-cell-expand-icon-cell {
+ text-align: center;
+
+ .fa-minus-square,
+ .fa-plus-square {
+ cursor: pointer;
+ }
+ }
+
+ > tbody > tr.#{$ns}Table-expanded-row > td {
+ background: var(--Table-onHover-bg);
+ }
+
+ > tfoot > tr > td {
+ padding: var(--TableCell-paddingY) var(--TableCell-paddingX);
+ }
+ }
+
+ &-container {
+ .#{$ns}Table-header {
+ padding: 0;
+ }
+ }
+
+ &.#{$ns}Table-ping-left {
+ .#{$ns}Table-table {
+ > thead > tr > th.#{$ns}Table-cell-fix-left-last,
+ > tbody > tr > td.#{$ns}Table-cell-fix-left-last,
+ > tfoot > tr > td.#{$ns}Table-cell-fix-left-last {
+ &:after {
+ box-shadow: var(--Table-fixedLeftLast-boxShadow);
+ }
+ }
+ }
+ }
+
+ &.#{$ns}Table-ping-right {
+ .#{$ns}Table-table {
+ > thead > tr > th.#{$ns}Table-cell-fix-right-first,
+ > tbody > tr > td.#{$ns}Table-cell-fix-right-first,
+ > tfoot > tr > td.#{$ns}Table-cell-fix-right-first {
+ &:after {
+ box-shadow: var(--Table-fixedRightFirst-boxShadow);
+ }
+ }
+
+ > thead > tr > th.#{$ns}Table-cell-fix-right-first {
+ border-right: var(--Table-thead-borderWidth) solid var(--Table-thead-borderColor);
+ }
+ }
+ }
+
+ &.#{$ns}Table-resizable {
+ .#{$ns}Table-table {
+ > thead > tr > th {
+ position: relative;
+
+ .#{$ns}Table-thead-resizable {
+ position: absolute;
+ width: 1px;
+ right: 0;
+ top: 0;
+ bottom: 0;
+ cursor: col-resize;
+ }
+ }
+ }
+ }
+
+ .#{$ns}Table-loading {
+ padding: var(--Table-loading-padding);
+ text-align: center;
}
&Cell-sortBtn {
@@ -658,6 +805,9 @@
color: var(--TableCell-sortBtn--onActive-color);
}
}
+ &:not(:last-child) {
+ right: calc(var(--TableCell-paddingX) - var(--TableCell-sortBtn-width) / 2 + 15px);
+ }
}
&Cell-searchBtn {
@@ -753,6 +903,20 @@
}
}
}
+
+ .#{$ns}DropDown-multiple-menu {
+ text-align: center;
+ border-top: 1px solid var(--Table-borderColor);
+
+ .#{$ns}Button {
+ margin: 0 5px;
+ padding: 0 10px;
+ }
+
+ &:hover {
+ background: none;
+ }
+ }
}
&-itemActions-wrap {
diff --git a/src/Schema.ts b/src/Schema.ts
index 26260632c..1decf0da5 100644
--- a/src/Schema.ts
+++ b/src/Schema.ts
@@ -200,6 +200,7 @@ export type SchemaType =
| 'switch'
| 'table'
| 'static-table' // 这个几个跟表单项同名,再form下面用必须带前缀 static-
+ | 'table-v2'
| 'tabs'
| 'html'
| 'tpl'
diff --git a/src/components/index.tsx b/src/components/index.tsx
index 2a42c52ce..cc87998d3 100644
--- a/src/components/index.tsx
+++ b/src/components/index.tsx
@@ -58,6 +58,7 @@ import TableSelection from './TableSelection';
import TreeSelection from './TreeSelection';
import AssociatedSelection from './AssociatedSelection';
import PullRefresh from './PullRefresh';
+import Table from './table';
export {
NotFound,
@@ -119,5 +120,6 @@ export {
NumberInput,
ArrayInput,
AnchorNav,
- PullRefresh
+ PullRefresh,
+ Table
};
diff --git a/src/components/table/Cell.tsx b/src/components/table/Cell.tsx
new file mode 100644
index 000000000..aa2948630
--- /dev/null
+++ b/src/components/table/Cell.tsx
@@ -0,0 +1,76 @@
+/**
+ * @file table/BodyCell
+ * @author fex
+ */
+
+import React from 'react';
+
+import {themeable, ThemeProps} from '../../theme';
+import {LocaleProps, localeable} from '../../locale';
+import {ColumnProps} from './index';
+
+const zIndex = 1;
+
+export interface Props extends ThemeProps, LocaleProps {
+ fixed?: string | boolean; // left | right
+ rowSpan?: number | any;
+ colSpan?: number | any;
+ key?: string | number;
+ className?: string;
+ children?: any;
+ tagName?: string;
+ style?: Object;
+ column?: ColumnProps
+}
+
+export class BodyCell extends React.Component {
+ static defaultProps = {
+ fixed: '',
+ rowSpan: null,
+ colSpan: null
+ };
+
+ render() {
+ const {
+ fixed,
+ rowSpan,
+ colSpan,
+ key,
+ children,
+ className,
+ tagName,
+ style,
+ column,
+ classnames: cx
+ } = this.props;
+
+ if (tagName === 'TH') {
+ return (
+ 1 ? rowSpan : null}
+ colSpan={colSpan && colSpan > 1 ? colSpan : null}
+ className={cx('Table-cell', className, {
+ [cx(`Table-cell-fix-${fixed}`)] : fixed
+ })}
+ style={fixed ? {position: 'sticky', zIndex} : style}
+ >{children} |
+ );
+ }
+
+ return (
+ 1 ? rowSpan : null}
+ colSpan={colSpan && colSpan > 1 ? colSpan : null}
+ className={cx('Table-cell', className, {
+ [cx(`Table-cell-fix-${fixed}`)] : fixed,
+ [`text-${column?.align}`] : column?.align
+ })}
+ style={fixed ? {position: 'sticky', zIndex} : {}}
+ >{children} |
+ );
+ }
+}
+
+export default themeable(localeable(BodyCell));
\ No newline at end of file
diff --git a/src/components/table/HeadCellFilter.tsx b/src/components/table/HeadCellFilter.tsx
new file mode 100644
index 000000000..adcb02e4d
--- /dev/null
+++ b/src/components/table/HeadCellFilter.tsx
@@ -0,0 +1,211 @@
+/**
+ * @file table/HeadCellFilter
+ * @author fex
+ */
+
+import React from 'react';
+import {findDOMNode} from 'react-dom';
+import isEqual from 'lodash/isEqual';
+
+import {themeable, ThemeProps} from '../../theme';
+import {LocaleProps, localeable} from '../../locale';
+import Overlay from '../Overlay';
+import PopOver from '../PopOver';
+import CheckBox from '../Checkbox';
+import Button from '../Button';
+import {Icon} from '../icons';
+
+export interface Props extends ThemeProps, LocaleProps {
+ column: any;
+ popOverContainer?: () => Element | Text | null;
+ onFilter?: Function;
+ onQuery?: Function;
+ filteredValue?: Array;
+ filterMultiple?: boolean;
+}
+
+export interface OptionProps {
+ text: string;
+ value: string;
+ selected?: boolean;
+ children?: Array;
+}
+
+export interface State {
+ options: Array;
+ isOpened: boolean;
+ filteredValue: Array;
+}
+
+export class HeadCellFilter extends React.Component {
+ static defaultProps = {
+ filteredValue: [],
+ filterMultiple: false
+ };
+
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ options: [],
+ isOpened: false,
+ filteredValue: props.filteredValue || []
+ }
+
+ this.openLayer = this.openLayer.bind(this);
+ this.closeLayer = this.closeLayer.bind(this);
+ }
+
+ alterOptions(options: Array) {
+ const {column} = this.props;
+
+ options = options.map(option => ({
+ ...option,
+ selected: this.state.filteredValue.indexOf(option.value) > -1
+ }));
+
+ return options;
+ }
+
+ componentDidMount() {
+ const {column} = this.props;
+ if (column.filters && column.filters.length > 0) {
+ this.setState({options: this.alterOptions(column.filters)});
+ }
+ }
+
+ componentDidUpdate(prevProps: Props, prevState: State) {
+ const {column} = this.props;
+ if (column.filters && column.filters.length > 0
+ && !isEqual(prevState.filteredValue, this.state.filteredValue)) {
+ this.setState({options: this.alterOptions(column.filters)});
+ }
+ }
+
+ render() {
+ const {isOpened, options} = this.state;
+ const {
+ column,
+ popOverContainer,
+ classnames: cx,
+ classPrefix: ns
+ } = this.props;
+
+ return (
+ item.selected) ? 'is-active' : ''
+ )}
+ >
+
+
+
+ {
+ isOpened ? (
+ findDOMNode(this))}
+ placement="left-bottom-left-top right-bottom-right-top"
+ target={
+ popOverContainer ? () => findDOMNode(this) : null
+ }
+ show
+ >
+
+ {options && options.length > 0 ? (
+
+ {!column.filterMultiple
+ ? options.map((option: any, index) => (
+ -
+ {option.text}
+
+ ))
+ : options.map((option: any, index) => (
+ -
+
+ {option.text}
+
+
+ ))}
+ {column.filterMultiple ? (
+ -
+
+
+
+ ) : null}
+
+ ) : null}
+
+ )
+ : null
+ }
+
+ );
+ }
+
+ openLayer() {
+ this.setState({isOpened: true});
+ }
+
+ closeLayer() {
+ this.setState({isOpened: false});
+ }
+
+ handleClick(value: string) {
+ const {onQuery, column} = this.props;
+
+ this.setState({filteredValue: [value]});
+
+ onQuery && onQuery({[column.key] : value});
+ this.closeLayer();
+ }
+
+ handleCheck(value: string) {
+ const filteredValue = this.state.filteredValue;
+ if (value) {
+ this.setState({filteredValue: [...filteredValue, value]});
+ } else {
+ this.setState({filteredValue: filteredValue.filter(v => v !== value)});
+ }
+ }
+
+ handleConfirmClick() {
+ const {onQuery, column} = this.props;
+ onQuery && onQuery({[column.key] : this.state.filteredValue});
+ this.closeLayer();
+ }
+
+ handleCancelClick() {
+ this.setState({filteredValue: []});
+ this.closeLayer();
+ }
+}
+
+export default themeable(localeable(HeadCellFilter));
\ No newline at end of file
diff --git a/src/components/table/HeadCellSort.tsx b/src/components/table/HeadCellSort.tsx
new file mode 100644
index 000000000..6c81f4bd6
--- /dev/null
+++ b/src/components/table/HeadCellSort.tsx
@@ -0,0 +1,102 @@
+/**
+ * @file table/HeadCellSort
+ * @author fex
+ */
+
+import React from 'react';
+
+import {themeable, ThemeProps} from '../../theme';
+import {LocaleProps, localeable} from '../../locale';
+import {Icon} from '../icons';
+
+export interface Props extends ThemeProps, LocaleProps {
+ column: any;
+ onSort?: Function;
+}
+
+export interface State {
+ order: string; // 升序还是降序
+ orderBy: string; // 一次只能按一列排序 当前列的key
+}
+
+export class HeadCellSort extends React.Component {
+ constructor(props: Props) {
+ super(props);
+
+ this.state = {
+ order: '',
+ orderBy: ''
+ };
+ }
+
+ render() {
+ const {
+ column,
+ onSort,
+ classnames: cx
+ } = this.props;
+
+ return (
+ {
+ const callback = () => {
+ if (onSort) {
+ onSort({
+ orderBy: this.state.orderBy,
+ order: this.state.order
+ });
+ }
+ }
+
+ let sortPayload = {};
+ if (column.key === this.state.orderBy) {
+ if (this.state.order === 'descend') {
+ // 降序改为取消
+ sortPayload = {orderBy: '', order: 'ascend'};
+ } else {
+ // 升序之后降序
+ sortPayload = {order: 'descend'};
+ }
+ } else {
+ // 默认先升序
+ sortPayload = {orderBy: column.key, order: 'ascend'};
+ }
+
+ this.setState(sortPayload, callback);
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default themeable(localeable(HeadCellSort));
\ No newline at end of file
diff --git a/src/components/table/index.tsx b/src/components/table/index.tsx
new file mode 100644
index 000000000..b2bdb110d
--- /dev/null
+++ b/src/components/table/index.tsx
@@ -0,0 +1,1192 @@
+/**
+ * @file Table
+ * @author fex
+ */
+
+import React from 'react';
+import {findDOMNode} from 'react-dom';
+import findLastIndex from 'lodash/findLastIndex';
+import find from 'lodash/find';
+import isEqual from 'lodash/isEqual';
+import filter from 'lodash/filter';
+import Sortable from 'sortablejs';
+
+import {themeable, ClassNamesFn, ThemeProps} from '../../theme';
+import {localeable, LocaleProps} from '../../locale';
+import {isObject} from '../../utils/helper';
+import {Icon} from '../icons';
+import CheckBox from '../Checkbox';
+import HeadCellSort from './HeadCellSort';
+import HeadCellFilter from './HeadCellFilter';
+import Cell from './Cell';
+
+export interface ColumnProps {
+ title: string | React.ReactNode | Function;
+ key: string;
+ className?: string;
+ children?: Array;
+ render: Function;
+ fixed?: boolean | string;
+ width?: number | string;
+ sorter?: (a: any, b: any) => number | boolean; // 设置为true时,执行onSort,否则执行前端排序
+ sortOrder?: string; // 升序ascend、降序descend
+ filters?: Array; // 筛选数据源,配置了数据源才展示
+ filterMode?: string; // menu/tree 默认menu 先只支持menu
+ filterMultiple?: boolean; // 是否支持多选
+ filteredValue?: Array;
+ filtered?: boolean;
+ align?: string; // left/right/center
+}
+
+export interface ThProps extends ColumnProps {
+ rowSpan: number;
+ colSpan: number;
+}
+
+export interface TdProps extends ColumnProps {
+ rowSpan: number;
+ colSpan: number;
+}
+
+export interface RowSelectionProps {
+ type: string;
+ fixed: boolean; // 只能固定在左边
+ selectedRowKeys: Array;
+ keyField?: string; // 默认是key,可自定义
+ columnWidth?: number;
+ onChange: Function;
+ onSelect: Function;
+ onSelectAll: Function;
+ getCheckboxProps: Function;
+}
+
+export interface ExpandableProps {
+ expandedRowKeys?: Array;
+ keyField: string; // 默认是key,可自定义
+ columnWidth?: number;
+ rowExpandable: Function;
+ defaultExpandedRowKeys?: Array;
+ onExpand?: Function;
+ onExpandedRowsChange?: Function;
+ expandedRowRender?: Function;
+ expandedRowClassName?: Function;
+ expandIcon?: Function;
+ fixed?: boolean;
+}
+
+export interface SummaryProps {
+ colSpan: number; // 手动控制列合并 先不支持列合并
+ fixed: string | boolean; // 手动设置左固定还是右固定
+ render: Function | React.ReactNode;
+}
+
+export interface ExchangeRecord {
+ [index: number]: number
+}
+
+export interface TableProps extends ThemeProps, LocaleProps {
+ title: string | React.ReactNode | Function;
+ footer: string | React.ReactNode | Function;
+ className?: string;
+ dataSource: Array;
+ classnames: ClassNamesFn
+ columns: Array;
+ scroll?: ScrollProps;
+ rowSelection?: RowSelectionProps,
+ onSort?: Function;
+ expandable?: ExpandableProps;
+ bordered?: boolean;
+ size?: string; // default | middle | small
+ headSummary?: Function | React.ReactNode | Array>;
+ footSummary?: Function | React.ReactNode | Array>;
+ draggable?: boolean;
+ resizable?: boolean; // 列宽调整
+ placeholder?: string | React.ReactNode | Function; // 数据为空展示
+ loading?: boolean; // 数据加载中
+}
+
+export interface ScrollProps {
+ x: number | string | true;
+ y: number | string;
+}
+
+export interface TableState {
+ selectedRowKeys: Array;
+ selectedRows: Array;
+ dataSource: Array;
+ expandedRowKeys: Array;
+}
+
+function getMaxLevelThRowSpan(columns: Array) {
+ let maxLevel = 0;
+ columns.forEach(c => {
+ const level = getThRowSpan(c);
+ if (maxLevel < level) {
+ maxLevel = level;
+ }
+ });
+ return maxLevel;
+}
+
+function getThRowSpan(column: ColumnProps) {
+ if (!column.children || (column.children && !column.children.length)) {
+ return 1;
+ }
+
+ return 1 + getMaxLevelThRowSpan(column.children);
+}
+
+function getThColSpan(column: ColumnProps) {
+ if (!column.children || (column.children && !column.children.length)) {
+ return 1;
+ }
+
+ return column.children.length;
+}
+
+function getThColumns(
+ columns: Array = [],
+ thColumns: Array>,
+ depth: number = 0,
+ fixed?: boolean | string
+) {
+ const ths: Array = [];
+ const maxLevel = getMaxLevelThRowSpan(columns);
+ // 在处理表头时,如果父级column设置了fixed属性,那么所有children保持一致
+ columns.forEach(column => {
+ let childMaxLevel = 0;
+ if (column.children) {
+ childMaxLevel = getMaxLevelThRowSpan(column.children);
+ }
+ const newColumn = {...column, rowSpan: maxLevel - childMaxLevel, colSpan: getThColSpan(column)};
+ if (fixed) {
+ newColumn.fixed = fixed;
+ }
+ const index = thColumns.length - depth;
+ if (thColumns[index]) {
+ thColumns[index].push(newColumn);
+ }
+ else {
+ ths.push(newColumn);
+ }
+ if (column.children) {
+ getThColumns(column.children, thColumns, depth + 1, column.fixed);
+ }
+ });
+ if (ths.length > 0) {
+ thColumns.unshift(ths);
+ }
+}
+
+export function getTdColumns(
+ columns: Array = [],
+ tds: Array = [],
+ fixed?: boolean | string
+) {
+ columns.forEach(column => {
+ if (column.children) {
+ getTdColumns(column.children, tds, column.fixed);
+ }
+ else {
+ // 如果父级设置了fixed 子级设置了 也是以父级的为主
+ if (fixed) {
+ column.fixed = fixed;
+ }
+ tds.push(column);
+ }
+ });
+}
+
+function isFixedLeftColumn(fixed: boolean | string | undefined) {
+ return fixed === true || fixed === 'left';
+}
+
+function isFixedRightColumn(fixed: boolean | string | undefined) {
+ return fixed === 'right';
+}
+
+function getPreviousLeftWidth(doms: HTMLCollection, index: number, columns: Array) {
+ let width = 0;
+ for (let i = 0; i < index; i++) {
+ if (columns && columns[i] && isFixedLeftColumn(columns[i].fixed)) {
+ const dom = doms[i] as HTMLElement;
+ width += dom.offsetWidth;
+ }
+ }
+ return width;
+}
+
+function getAfterRightWidth(doms: HTMLCollection, index: number, columns: Array) {
+ let width = 0;
+ for (let i = doms.length - 0; i > index; i--) {
+ if (columns && columns[i] && isFixedRightColumn(columns[i].fixed)) {
+ const dom = doms[i] as HTMLElement;
+ width += dom.offsetWidth;
+ }
+ }
+ return width;
+}
+
+function hasFixedColumn(columns: Array) {
+ return find(columns, column => column.fixed);
+}
+
+function getSummaryColumns(summary: Array) {
+ if (!summary) {
+ return [];
+ }
+ const last: Array = [];
+ const first: Array = [];
+ summary.forEach(item => {
+ if (isObject(item)) {
+ first.push(item);
+ } else if (Array.isArray(item)) {
+ last.push(item);
+ }
+ });
+ return [first, ...last];
+}
+
+export class Table extends React.PureComponent {
+ static defaultProps = {
+ title: '',
+ className: '',
+ dataSource: [],
+ columns: []
+ };
+
+ constructor(props: TableProps) {
+ super(props);
+
+ const selectedRows: Array = [];
+ if (props.rowSelection) {
+ props.dataSource.forEach(data => {
+ if (find(props.rowSelection?.selectedRowKeys,
+ key => key === data[props.rowSelection?.keyField || 'key'])) {
+ selectedRows.push(data);
+ }
+ });
+ }
+
+ this.state = {
+ selectedRowKeys: props.rowSelection ? (props.rowSelection.selectedRowKeys || []) : [],
+ selectedRows,
+ dataSource: props.dataSource || [],
+ expandedRowKeys: [
+ ...(props.expandable ? (props.expandable.expandedRowKeys || []) : []),
+ ...props.expandable ? (props.expandable.defaultExpandedRowKeys || []) : []
+ ]
+ };
+
+ this.exchangeRecord = {};
+
+ this.onRowMouseEnter = this.onRowMouseEnter.bind(this);
+ this.onRowMouseLeave = this.onRowMouseLeave.bind(this);
+ this.onTableContentScroll = this.onTableContentScroll.bind(this);
+ this.onTableScroll = this.onTableScroll.bind(this);
+ this.getPopOverContainer = this.getPopOverContainer.bind(this);
+ }
+
+ getPopOverContainer() {
+ return findDOMNode(this);
+ }
+
+ // 记录顺序调整
+ exchangeRecord: ExchangeRecord;
+ tdColumns: Array;
+ thColumns: Array>;
+ sortable: Sortable;
+ // 记录点击起始横坐标
+ resizeStart: number;
+ resizeKey: string;
+
+ tableDom: React.RefObject = React.createRef();
+ theadDom: React.RefObject = React.createRef();
+ tbodyDom: React.RefObject = React.createRef();
+ contentDom: React.RefObject = React.createRef();
+ headerDom: React.RefObject = React.createRef();
+ bodyDom: React.RefObject = React.createRef();
+ footDom: React.RefObject = React.createRef();
+
+ updateTableBodyFixed() {
+ const tbodyDom = this.tbodyDom && (this.tbodyDom.current as HTMLElement);
+ const tdColumns = [...this.tdColumns];
+ this.updateTbodyFixedRow(tbodyDom, tdColumns);
+ this.updateHeadSummaryFixedRow(tbodyDom);
+ }
+
+ componentDidMount() {
+ if (this.props.loading) {
+ return;
+ }
+ if (hasFixedColumn(this.props.columns)) {
+ const theadDom = this.theadDom && (this.theadDom.current as HTMLElement);
+ const thColumns = this.thColumns;
+ this.updateTheadFixedRow(theadDom, thColumns);
+ const headerDom = this.headerDom && (this.headerDom.current as HTMLElement);
+ if (headerDom) {
+ const headerBody = headerDom.getElementsByTagName('tbody');
+ headerBody && headerBody[0] && this.updateHeadSummaryFixedRow(headerBody[0]);
+ }
+
+ // 同步数据 dom加载后直接更新
+ this.updateTableBodyFixed();
+
+ const footDom = this.footDom && (this.footDom.current as HTMLElement);
+ footDom && this.updateFootSummaryFixedRow(footDom);
+ }
+
+ let current = null;
+ if (this.contentDom && this.contentDom.current) {
+ current = this.contentDom.current;
+ current.addEventListener('scroll', this.onTableContentScroll.bind(this));
+ } else {
+ current = this.headerDom?.current;
+ this.headerDom && this.headerDom.current
+ && this.headerDom.current.addEventListener('scroll', this.onTableScroll.bind(this));
+ this.bodyDom && this.bodyDom.current
+ && this.bodyDom.current.addEventListener('scroll', this.onTableScroll.bind(this));
+ }
+ current && this.updateTableDom(current);
+
+ if (this.props.draggable) {
+ this.initDragging();
+ }
+
+ if (this.props.resizable) {
+ this.theadDom.current?.addEventListener('mouseup', this.onResizeMouseUp.bind(this));
+ }
+ }
+
+ componentDidUpdate(prevProps: TableProps, prevState: TableState) {
+ // 数据源发生了变化
+ if (!isEqual(prevProps.dataSource, this.props.dataSource)) {
+ this.setState({dataSource: [...this.props.dataSource]},
+ () => {
+ if (hasFixedColumn(this.props.columns)) {
+ this.updateTableBodyFixed();
+ }
+ }); // 异步加载数据需求再更新一次
+ }
+ // 选择项发生了变化触发
+ if (!isEqual(prevState.selectedRowKeys, this.state.selectedRowKeys)) {
+ const {rowSelection} = this.props;
+ rowSelection && rowSelection.onChange
+ && rowSelection.onChange(this.state.selectedRowKeys, this.state.selectedRows);
+ }
+ // 展开行变化时触发
+ if (!isEqual(prevState.expandedRowKeys, this.state.expandedRowKeys)) {
+ if (this.props.expandable) {
+ const {onExpandedRowsChange, keyField} = this.props.expandable;
+ const expandedRows: Array = [];
+ this.state.dataSource.forEach(item => {
+ if (find(this.state.expandedRowKeys, key => key === item[keyField || 'key'])) {
+ expandedRows.push(item);
+ }
+ });
+ onExpandedRowsChange && onExpandedRowsChange(expandedRows);
+ }
+ }
+ }
+
+ componentWillUnmount() {
+ this.contentDom && this.contentDom.current
+ && this.contentDom.current.removeEventListener('scroll', this.onTableContentScroll.bind(this));
+ this.headerDom && this.headerDom.current
+ && this.headerDom.current.removeEventListener('scroll', this.onTableScroll.bind(this));
+ this.bodyDom && this.bodyDom.current
+ && this.bodyDom.current.removeEventListener('scroll', this.onTableScroll.bind(this));
+
+ this.destroyDragging();
+ }
+
+ exchange(fromIndex: number, toIndex: number, item?: any) {
+ const {scroll, headSummary} = this.props;
+ // 如果有头部总结行 fromIndex就会+1
+ if ((!scroll || scroll && !scroll.y) && headSummary) {
+ fromIndex = fromIndex - 1;
+ }
+
+ // 记录下交换顺序 估计会有用
+ // 本身sortable就更新视图了 就不要再setState触发试图更新了 会有问题
+ this.exchangeRecord[fromIndex] = toIndex;
+ }
+
+ initDragging() {
+ const {classnames: cx} = this.props;
+
+ this.sortable = new Sortable(
+ this.tbodyDom.current as HTMLElement,
+ {
+ group: 'table',
+ animation: 150,
+ handle: `.${cx('Table-dragCell')}`,
+ ghostClass: 'is-dragging',
+ onMove: (e: any) => {
+ if (e.related && e.related.classList.contains(`${cx('Table-summary-row')}`)) {
+ return false;
+ }
+ return true;
+ },
+ onEnd: (e: any) => {
+ // 没有移动
+ if (e.newIndex === e.oldIndex) {
+ return;
+ }
+
+ this.exchange(e.oldIndex, e.newIndex);
+ }
+ }
+ );
+ }
+
+ destroyDragging() {
+ this.sortable && this.sortable.destroy();
+ }
+
+ // 更新一个tr下的td的left和class
+ updateFixedRow(row: HTMLElement, columns: Array) {
+ const {classnames: cx} = this.props;
+
+ const children = row.children;
+ for (let i = 0; i < children.length; i++) {
+ const dom = children[i] as HTMLElement;
+ const fixed = columns[i] ? (columns[i].fixed || '') : '';
+ if (isFixedLeftColumn(fixed)) {
+ dom.style.left = i > 0 ? getPreviousLeftWidth(children, i, columns) + 'px' : '0';
+ } else if (isFixedRightColumn(fixed)) {
+ dom.style.right = i < children.length - 1 ? getAfterRightWidth(children, i, columns) + 'px' : '0';
+ }
+ }
+ // 最后一个左fixed的添加样式
+ let leftIndex = findLastIndex(columns, column => isFixedLeftColumn(column.fixed));
+ if (leftIndex > -1) {
+ children[leftIndex]?.classList.add(cx('Table-cell-fix-left-last'));
+ }
+ // 第一个右fixed的添加样式
+ let rightIndex = columns.findIndex(column => isFixedRightColumn(column.fixed));
+ if (rightIndex > -1) {
+ children[rightIndex]?.classList.add(cx('Table-cell-fix-right-first'));
+ }
+ }
+
+ // 在可选、可展开、可拖拽的情况下,补充column,方便fix处理
+ prependColumns(columns: Array) {
+ const {rowSelection, expandable, draggable} = this.props;
+ if (draggable) {
+ columns.unshift({});
+ } else {
+ if (expandable) {
+ columns.unshift(expandable);
+ }
+ if (rowSelection) {
+ columns.unshift(rowSelection);
+ }
+ }
+ }
+
+ updateTheadFixedRow(thead: HTMLElement, columns: Array) {
+ const children = thead.children;
+ for (let i = 0; i < children.length; i++) {
+ const cols = [...columns[i]];
+ if (i === 0) {
+ this.prependColumns(cols);
+ }
+
+ this.updateFixedRow(children[i] as HTMLElement, cols);
+ }
+ }
+
+ updateTbodyFixedRow(tbody: HTMLElement, columns: Array) {
+ const {classnames: cx} = this.props;
+ const children = filter(tbody.children,
+ child => !child.classList.contains(cx('Table-summary-row'))
+ && !child.classList.contains(cx('Table-empty-row')));
+ this.prependColumns(columns);
+ for (let i = 0; i < children.length; i++) {
+ this.updateFixedRow(children[i] as HTMLElement, columns);
+ }
+ }
+
+ updateSummaryFixedRow(children: HTMLCollection | Array, columns: Array) {
+ for (let i = 0; i < children.length; i++) {
+ this.updateFixedRow(children[i] as HTMLElement, columns[i]);
+ }
+ }
+
+ updateFootSummaryFixedRow(tfoot: HTMLElement) {
+ const {footSummary} = this.props;
+ if (Array.isArray(footSummary)) {
+ const columns = getSummaryColumns(footSummary as Array);
+ this.updateSummaryFixedRow(tfoot.children, columns);
+ }
+ }
+
+ updateHeadSummaryFixedRow(tbody: HTMLElement) {
+ const {headSummary, classnames: cx} = this.props;
+ if (Array.isArray(headSummary)) {
+ const columns = getSummaryColumns(headSummary as Array);
+ const children = filter(tbody.children,
+ child => child.classList.contains(cx('Table-summary-row')));
+ this.updateSummaryFixedRow(children, columns);
+ }
+ }
+
+ renderColGroup() {
+ const {rowSelection, classnames: cx, expandable, draggable} = this.props;
+
+ const tdColumns = this.tdColumns;
+ const isExpandable = !!expandable;
+
+ return (
+
+ {draggable
+ ? : null}
+ {!draggable && rowSelection && rowSelection.type
+ ? : null}
+ {
+ !draggable && isExpandable ? : null
+ }
+ {tdColumns.map((data, index) => {
+ const width = data.width ? +data.width : data.width;
+ return
+ ;
+ })}
+
+ );
+ }
+
+ onResizeMouseDown(event: React.MouseEvent, key: string) {
+ // 点击记录起始坐标
+ this.resizeStart = event.clientX;
+ // 记录点击的列名
+ this.resizeKey = key;
+ event && event.stopPropagation();
+ }
+
+ onResizeMouseUp(event: React.MouseEvent) {
+ // 点击了调整列宽
+ if (this.resizeStart && this.resizeKey) {
+ // 计算横向移动距离
+ const distance = event.clientX - this.resizeStart;
+ const tdColumns = [...this.tdColumns];
+ let index = tdColumns.findIndex(c => c.key === this.resizeKey) + this.getExtraColumnCount();
+
+ const colGroup = this.tableDom.current?.getElementsByTagName('colgroup')[0];
+ let currentWidth = 0;
+ if (colGroup && colGroup.children[index]) {
+ const child = colGroup.children[index] as HTMLElement;
+ currentWidth = child.offsetWidth;
+ }
+
+ const column = find(tdColumns, c => c.key === this.resizeKey);
+ if (column) {
+ column.width = currentWidth + distance;
+ }
+
+ this.tdColumns = tdColumns;
+
+ this.resizeStart = 0;
+ this.resizeKey = '';
+ }
+ event && event.stopPropagation();
+ }
+
+ renderTHead() {
+ const {
+ rowSelection,
+ dataSource,
+ classnames: cx,
+ onSort,
+ expandable,
+ draggable,
+ resizable
+ } = this.props;
+
+ const thColumns = this.thColumns;
+ const keyField = rowSelection ? (rowSelection.keyField || 'key') : '';
+ const dataList = rowSelection && rowSelection.getCheckboxProps
+ ? this.state.dataSource.filter(data => {
+ const props = rowSelection.getCheckboxProps(data);
+ return !props.disabled;
+ }) : this.state.dataSource;
+
+ const isExpandable = !!expandable;
+
+ return (
+
+ {thColumns.map((data, index) => {
+ return
+ {
+ draggable && index === 0 ? | : null
+ }
+ {!draggable && rowSelection && index === 0
+ ?
+ {rowSelection.type !== 'radio'
+ ? 0 && this.state.selectedRowKeys.length < dataList.length}
+ checked={this.state.selectedRowKeys.length > 0}
+ onChange={value => {
+ let changeRows;
+ if (value) {
+ changeRows = dataList.filter(data => !find(this.state.selectedRowKeys,
+ key => key === data[keyField]));
+ } else {
+ changeRows = this.state.selectedRows;
+ }
+ const selectedRows = value ? dataList : [];
+ this.setState({
+ selectedRowKeys: value ? dataList.map(data => data[keyField]) : [],
+ selectedRows
+ });
+
+ rowSelection.onSelectAll && rowSelection.onSelectAll(value, selectedRows, changeRows);
+ }}> : null
+ } | : null}
+ {
+ !draggable && isExpandable && index === 0
+ ? | : null
+ }
+ {data.map((item, i) => {
+ let sort = null;
+ if (item.sorter) {
+ sort = (
+ {
+ if (typeof item.sorter === 'function') {
+ if (payload.orderBy) {
+ const sortList = [...this.state.dataSource];
+ this.setState({dataSource: sortList.sort(item.sorter as (a: any, b: any) => number)});
+ } else {
+ this.setState({dataSource: [...dataSource]});
+ }
+ }
+ }}
+ >
+ );
+ }
+
+ let filter = null;
+ if (item.filters && item.filters.length > 0) {
+ filter = (
+
+
+ );
+ }
+
+ return
+ {typeof item.title === 'function' ? item.title() : item.title}
+ {sort}
+ {filter}
+ {resizable ? this.onResizeMouseDown(e, item.key)}> : null} | ;
+ })
+ }
;
+ })}
+
+ );
+ }
+
+ onRowMouseEnter(event: React.ChangeEvent) {
+ const {classnames: cx} = this.props;
+
+ let parent = event.target;
+ while (parent.tagName !== 'TR') {
+ parent = parent.parentNode;
+ }
+ if (parent) {
+ for (let i = 0; i < parent.children.length; i++) {
+ const td = parent.children[i];
+ td.classList.add(cx(`Table-cell-row-hover`));
+ }
+ }
+ }
+
+ onRowMouseLeave(event: React.ChangeEvent) {
+ const {classnames: cx} = this.props;
+
+ let parent = event.target;
+ while (parent.tagName !== 'TR') {
+ parent = parent.parentNode;
+ }
+ if (parent) {
+ for (let i = 0; i < parent.children.length; i++) {
+ const td = parent.children[i];
+ td.classList.remove(cx(`Table-cell-row-hover`));
+ }
+ }
+ }
+
+ onExpandRow(data: any) {
+ const {expandedRowKeys} = this.state;
+ const {expandable} = this.props;
+ const key = data[expandable?.keyField || 'key'];
+ this.setState({expandedRowKeys: [...expandedRowKeys, key]});
+ expandable?.onExpand && expandable?.onExpand(true, data);
+ }
+
+ onCollapseRow(data: any) {
+ const {expandedRowKeys} = this.state;
+ const {expandable} = this.props;
+ const key = data[expandable?.keyField || 'key'];
+ // 还是得模糊匹配 否则'3'、3匹配不上
+ this.setState({expandedRowKeys: expandedRowKeys.filter(k => k != key)});
+ expandable?.onExpand && expandable?.onExpand(false, data);
+ }
+
+ renderTBody() {
+ const {
+ classnames: cx,
+ rowSelection,
+ expandable,
+ headSummary,
+ scroll,
+ draggable,
+ placeholder
+ } = this.props;
+
+ const tdColumns = this.tdColumns;
+ const defaultKey = rowSelection ? (rowSelection.keyField || 'key') : '';
+ const isExpandable = !!expandable;
+ const hasScrollY = scroll && scroll.y;
+ const colCount = this.getExtraColumnCount();
+ return (
+
+ {!hasScrollY && headSummary ? this.renderSummaryRow(headSummary) : null}
+ {
+ !this.state.dataSource.length
+ ?
+
+
+ {typeof placeholder === 'function' ? placeholder() : placeholder}
+
+ |
+
+ : this.state.dataSource.map((data, index) => {
+ // 当前行是否可展开
+ const expandableRow = expandable && expandable.rowExpandable
+ && expandable.rowExpandable(data);
+
+ const cells = tdColumns.map((item, i) => {
+ const render = item.render && typeof item.render === 'function'
+ ? item.render(data[item.key], data, index, i) : null;
+ let props = {rowSpan: 1, colSpan: 1};
+ let children = render;
+ if (render && isObject(render)) {
+ props = render.props;
+ children = render.children;
+ // 如果合并行 且有展开行,那么合并行不生效
+ if (props.rowSpan > 1 && expandableRow) {
+ props.rowSpan === 1;
+ }
+ }
+ return props.rowSpan === 0 || props.colSpan === 0 ? null :
+ {item.render && typeof item.render === 'function'
+ ? children
+ : data[item.key]}
+ | ;
+ });
+
+ // 支持拖拽排序 可选、可展开都先不支持了
+ if (draggable) {
+ return
+
+
+ | {cells}
;
+ }
+
+ const checkboxProps = rowSelection && rowSelection.getCheckboxProps
+ ? rowSelection.getCheckboxProps(data) : {};
+ const isExpanded = !!find(this.state.expandedRowKeys,
+ key => key == data[expandable?.keyField || 'key']); // == 匹配 否则'3'、3匹配不上
+
+ const expandedRowClassName = expandable && expandable.expandedRowClassName
+ && typeof expandable.expandedRowClassName === 'function'
+ ? expandable.expandedRowClassName(data, index) : ''
+
+ return [
+ {rowSelection
+ ? (
+ key === data[defaultKey])}
+ onChange={
+ (value, shift) => {
+ const isRadio = rowSelection.type === 'radio';
+
+ const callback = () => {
+ rowSelection.onSelect
+ && rowSelection.onSelect(data, value, this.state.selectedRows);
+ };
+
+ if (value) {
+ if (isRadio) {
+ this.setState({
+ selectedRowKeys: [data[defaultKey]],
+ selectedRows: [data]
+ }, callback);
+ } else {
+ this.setState(prevState => ({
+ selectedRowKeys: [...prevState.selectedRowKeys, data[defaultKey]],
+ selectedRows: [...prevState.selectedRows, data]
+ }), callback);
+ }
+ } else {
+ if (!isRadio) {
+ this.setState({
+ selectedRowKeys: this.state.selectedRowKeys.filter(key => key !== data[defaultKey]),
+ selectedRows: this.state.selectedRows.filter(item => item[defaultKey] !== data[defaultKey])
+ }, callback);
+ }
+ }
+
+ event && event.stopPropagation();
+ }
+ }
+ {...checkboxProps}> | ) : null}
+ {
+ isExpandable ?
+ {expandableRow ? (isExpanded
+ ?
+ : ) : null}
+ | : null
+ }
+ {cells}
, expandableRow
+ ?
+
+ {expandable.expandedRowRender
+ && typeof expandable.expandedRowRender === 'function'
+ ? expandable.expandedRowRender(data, index) : null}
+ |
+
: null];
+ })
+ }
+
+ );
+ }
+
+ getExtraColumnCount() {
+ const {draggable, expandable, rowSelection} = this.props;
+ let count = 0;
+ if (draggable) {
+ count++;
+ } else {
+ if (expandable) {
+ count++;
+ }
+ if (rowSelection) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ renderSummaryRow(summary: any) {
+ const {classnames: cx, dataSource} = this.props;
+ const cells: Array = [];
+ const trs: Array = [];
+ let colCount = this.getExtraColumnCount();
+
+ (Array.isArray(summary) ? summary.map((s, index) => {
+ return (
+ Array.isArray(s) ? trs.push(
+ {s.map((d, i) => {
+ // 将操作列自动添加到第一列,用户的colSpan只需要关心实际的列数
+ const colSpan = i === 0 ? (d.colSpan || 1) + colCount : d.colSpan;
+ return
+ {typeof d.render === 'function' ? d.render(dataSource) : d.render}
+ | ;
+ })}
) : cells.push(
+
+ {typeof s.render === 'function' ? s.render(dataSource) : s.render}
+ | )
+ )
+ }) : null)
+ return (
+ summary ? (typeof summary === 'function'
+ ? summary(dataSource) : [
+ {cells}
, trs
+ ]) : null
+ );
+ }
+
+ renderTFoot() {
+ const {classnames: cx, footSummary} = this.props;
+ return (
+ {this.renderSummaryRow(footSummary)}
+ );
+ }
+
+ updateTableDom(dom: HTMLDivElement) {
+ const {classnames: cx} = this.props;
+ const {scrollLeft, scrollWidth, offsetWidth} = dom;
+ const table = this.tableDom.current;
+
+ const leftCalss = cx('Table-ping-left');
+ if (scrollLeft > 0) {
+ table?.classList.add(leftCalss);
+ } else {
+ table?.classList.remove(leftCalss);
+ }
+
+ const rightClass = cx('Table-ping-right');
+ if (scrollLeft + offsetWidth < scrollWidth) {
+ table?.classList.add(rightClass);
+ } else {
+ table?.classList.remove(rightClass);
+ }
+ }
+
+ onTableContentScroll(event: React.ChangeEvent) {
+ this.updateTableDom(event.target);
+ }
+
+ onTableScroll(event: React.ChangeEvent) {
+ const headerCurrent = this.headerDom.current;
+ const bodyCurrent = this.bodyDom.current;
+ const target = event.target;
+ if (target === bodyCurrent && headerCurrent) {
+ headerCurrent.scrollLeft = target.scrollLeft;
+ } else if (target === headerCurrent && bodyCurrent) {
+ bodyCurrent.scrollLeft = target.scrollLeft;
+ }
+
+ this.updateTableDom(target);
+ }
+
+ renderLoading() {
+ const {classnames: cx} = this.props;
+ return 加载中
;
+ }
+
+ renderTable() {
+ const {
+ scroll,
+ footSummary,
+ loading,
+ classnames: cx
+ } = this.props;
+
+ // 设置了横向滚动轴 则table的table-layout为fixed
+ const hasScrollX = scroll && scroll.x;
+
+ return (
+
+
+ {this.renderColGroup()}
+ {this.renderTHead()}
+ {!loading ? this.renderTBody() : null}
+ {!loading && footSummary ? this.renderTFoot() : null}
+
+ {loading ? this.renderLoading() : null}
+
+ );
+ }
+
+ renderScrollTableHeader() {
+ const {
+ scroll,
+ headSummary,
+ classnames: cx
+ } = this.props;
+
+ return (
+
+
+ {this.renderColGroup()}
+ {this.renderTHead()}
+ {headSummary ? {this.renderSummaryRow(headSummary)} : null}
+
+
+ );
+ }
+
+ renderScrollTableBody() {
+ const {
+ scroll,
+ classnames: cx
+ } = this.props;
+
+ return (
+
+
+ {this.renderColGroup()}
+ {this.renderTBody()}
+
+
+ )
+ }
+
+ renderScrollTableFoot() {
+ const {
+ scroll,
+ classnames: cx
+ } = this.props;
+
+ return (
+
+ );
+ }
+
+ renderScrollTable() {
+ const {
+ footSummary,
+ loading,
+ classnames: cx
+ } = this.props;
+
+ return (
+
+ {this.renderScrollTableHeader()}
+ {!loading ? this.renderScrollTableBody() : null}
+ {!loading && footSummary ? this.renderScrollTableFoot() : null}
+ {loading ? this.renderLoading() : null}
+
+ );
+ }
+
+ render() {
+ const {
+ title,
+ footer,
+ className,
+ scroll,
+ size,
+ bordered,
+ resizable,
+ columns,
+ classnames: cx
+ } = this.props;
+
+ this.thColumns = [];
+ this.tdColumns = [];
+ getThColumns(columns, this.thColumns);
+ getTdColumns(columns, this.tdColumns);
+
+ // 是否设置了纵向滚动
+ const hasScrollY = scroll && scroll.y;
+ // 是否设置了横向滚动
+ const hasScrollX = scroll && scroll.x;
+
+ return (
+
+ {title ?
{
+ typeof title === 'function' ? title() : title
+ }
: ''}
+
+ {hasScrollY ? this.renderScrollTable()
+ :
{this.renderTable()}
}
+
+ {footer ?
{
+ typeof footer === 'function' ? footer() : footer
+ }
: ''}
+
+ );
+ }
+}
+
+export default themeable(
+ localeable(Table)
+);
diff --git a/src/index.tsx b/src/index.tsx
index 87ecdc135..ac9f7c5d5 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -178,6 +178,7 @@ import './renderers/Code';
import './renderers/WebComponent';
import './renderers/GridNav';
import './renderers/TooltipWrapper';
+import './renderers/Table-v2';
import Scoped, {ScopedContext} from './Scoped';
diff --git a/src/renderers/QuickEdit.tsx b/src/renderers/QuickEdit.tsx
index a21801afe..7a3857d0b 100644
--- a/src/renderers/QuickEdit.tsx
+++ b/src/renderers/QuickEdit.tsx
@@ -322,11 +322,14 @@ export const HocQuickEdit =
);
}
- openQuickEdit() {
+ openQuickEdit(e) {
currentOpened = this;
this.setState({
isOpened: true
});
+ // QuickEdit在table中使用时,如果table配置了checkOnItemClick,会同时触发行选中
+ // 所以这里阻止冒泡一下
+ e.stopPropagation && e.stopPropagation();
}
closeQuickEdit() {
diff --git a/src/renderers/Table-v2/index.tsx b/src/renderers/Table-v2/index.tsx
new file mode 100644
index 000000000..d98e7c2da
--- /dev/null
+++ b/src/renderers/Table-v2/index.tsx
@@ -0,0 +1,483 @@
+import React from 'react';
+import cloneDeep from 'lodash/cloneDeep';
+
+import {Renderer, RendererProps} from '../../factory';
+import {SchemaNode, Schema} from '../../types';
+import Table, {ColumnProps} from '../../components/table';
+import {
+ BaseSchema,
+ SchemaObject,
+ SchemaTokenizeableString
+} from '../../Schema';
+import {isObject} from '../../utils/helper';
+import {
+ resolveVariableAndFilter
+} from '../../utils/tpl-builtin';
+import {evalExpression, filter} from '../../utils/tpl';
+import {Icon} from '../../components/icons';
+import Checkbox from '../../components/Checkbox';
+import {TableStoreV2, ITableStore, IColumn} from '../../store/table-v2';
+import ColumnToggler from '../Table/ColumnToggler';
+
+/**
+ * Table 表格v2渲染器。
+ * 文档:https://baidu.gitee.io/amis/docs/components/table-v2
+ */
+
+export interface CellSpan {
+ colIndex: number;
+ rowIndex: number;
+ colSpan?: number;
+ rowSpan?: number;
+}
+
+export interface RenderProps {
+ colSpan?: number;
+ rowSpan?: number;
+}
+
+export interface ColumnSchema {
+ /**
+ * 指定列唯一标识
+ */
+ key: string;
+
+ /**
+ * 指定列标题
+ */
+ title: string | SchemaObject;
+
+ /**
+ * 指定列内容渲染器
+ */
+ type?: string;
+
+ /**
+ * 指定行合并表达式
+ */
+ rowSpanExpr?: string;
+
+ /**
+ * 指定列合并表达式
+ */
+ colSpanExpr?: string;
+
+ /**
+ * 表头分组
+ */
+ children?: Array
+}
+
+export interface RowSelectionSchema {
+ /**
+ * 选择类型 单选/多选
+ */
+ type: string;
+
+ /**
+ * 对应数据源的key值
+ */
+ keyField: string;
+
+ /**
+ * 行是否禁用表达式
+ */
+ disableOn: string;
+}
+
+export interface ExpandableSchema {
+ /**
+ * 渲染器类型
+ */
+ type: string;
+
+ /**
+ * 对应数据源的key值
+ */
+ keyField: string;
+
+ /**
+ * 行是否可展开表达式
+ */
+ expandableOn: string;
+
+ /**
+ * 展开行自定义样式表达式
+ */
+ expandedRowClassNameExpr: string;
+}
+
+export interface TableSchema extends BaseSchema {
+ /**
+ * 指定为表格类型
+ */
+ type: 'table-v2';
+
+ /**
+ * 表格标题
+ */
+ title: string | SchemaObject;
+
+ /**
+ * 表格数据源
+ */
+ source: SchemaTokenizeableString;
+
+ /**
+ * 表格可自定义列
+ */
+ columnsToggable: boolean;
+
+ /**
+ * 表格列配置
+ */
+ columns: Array;
+
+ /**
+ * 表格可选择配置
+ */
+ rowSelection: RowSelectionSchema;
+
+ /**
+ * 表格行可展开配置
+ */
+ expandable: ExpandableSchema;
+}
+
+export interface TableV2Props extends RendererProps {
+ title?: string;
+ source?: string;
+ store: ITableStore;
+ togglable: boolean;
+}
+
+@Renderer({
+ type: 'table-v2',
+ storeType: TableStoreV2.name,
+ name: 'table-v2'
+})
+export class TableRenderer extends React.Component {
+ renderedToolbars: Array = [];
+
+ constructor(props: TableV2Props) {
+ super(props);
+
+ this.handleColumnToggle = this.handleColumnToggle.bind(this);
+
+ const {store, columnsToggable, columns} = props;
+
+ store.update({columnsToggable, columns});
+ }
+
+ renderSchema(schema: any, props: any) {
+ const {render} = this.props;
+ // Table Header、Footer SchemaObject转化成ReactNode
+ if (schema && isObject(schema)) {
+ return render('field', schema, props);
+ }
+ return schema;
+ }
+
+ getColumns(columns: Array) {
+ const cols: Array = [];
+ const rowSpans: Array = [];
+ const colSpans: Array = [];
+ columns.forEach((column, col) => {
+ const clone = {...column} as ColumnProps;
+ if (isObject(column.title)) {
+ const title = cloneDeep(column.title);
+ Object.assign(clone, {
+ title: () => this.renderSchema(title, {})
+ });
+ } else if (typeof column.title === 'string') {
+ Object.assign(clone, {
+ title: () => this.renderSchema({type: 'plain'}, {value: column.title})
+ });
+ }
+
+ if (column.type) {
+ Object.assign(clone, {
+ render: (text: string, record: any, rowIndex: number, colIndex: number) => {
+ const props: RenderProps = {};
+ const obj = {
+ children: this.renderSchema(column, {
+ data: record,
+ value: record[column.key]
+ }),
+ props
+ };
+ if (column.rowSpanExpr) {
+ const rowSpan = +filter(column.rowSpanExpr, {record, rowIndex, colIndex});
+ if (rowSpan) {
+ obj.props.rowSpan = rowSpan;
+ rowSpans.push({colIndex, rowIndex, rowSpan})
+ }
+ }
+
+ if (column.colSpanExpr) {
+ const colSpan = +filter(column.colSpanExpr, {record, rowIndex, colIndex});
+ if (colSpan) {
+ obj.props.colSpan = colSpan;
+ colSpans.push({colIndex, rowIndex, colSpan});
+ }
+ }
+
+ rowSpans.forEach(item => {
+ if (colIndex === item.colIndex
+ && rowIndex > item.rowIndex
+ && rowIndex < item.rowIndex + (item.rowSpan || 0)) {
+ obj.props.rowSpan = 0;
+ }
+ });
+
+ colSpans.forEach(item => {
+ if (rowIndex === item.rowIndex
+ && colIndex > item.colIndex
+ && colIndex < item.colIndex + (item.colSpan || 0)) {
+ obj.props.colSpan = 0;
+ }
+ });
+
+ return obj;
+ }
+ });
+ }
+
+ if (column.children) {
+ clone.children = this.getColumns(column.children);
+ }
+
+ cols.push(clone);
+ });
+ return cols;
+ }
+
+ getSummary(summary: Array) {
+ const result: Array = [];
+ if (Array.isArray(summary)) {
+ summary.forEach((s, index) => {
+ if (isObject(s)) {
+ result.push({
+ colSpan: s.colSpan,
+ fixed: s.fixed,
+ render: () => this.renderSchema(s, {
+ data: this.props.data
+ })
+ });
+ } else if (Array.isArray(s)) {
+ if (!result[index]) {
+ result.push([]);
+ }
+ s.forEach(d => {
+ result[index].push({
+ colSpan: d.colSpan,
+ fixed: d.fixed,
+ render: () => this.renderSchema(s, {
+ data: this.props.data
+ })
+ });
+ });
+ }
+ })
+ }
+
+ return result.length ? result : null;
+ }
+
+ handleColumnToggle(columns: Array) {
+ const {store} = this.props;
+
+ store.update({columns});
+ }
+
+ renderColumnsToggler(config?: any) {
+ const {
+ className,
+ store,
+ render,
+ classPrefix: ns,
+ classnames: cx,
+ ...rest
+ } = this.props;
+ const __ = rest.translate;
+ const env = rest.env;
+
+ if (!store.toggable) {
+ return null;
+ }
+
+ return (
+
+ }
+ draggable={config?.draggable}
+ columns={store.columnsData}
+ onColumnToggle={this.handleColumnToggle}
+ >
+ {store.toggableColumns.map(column => (
+
+
+ {column.title ? render('tpl', column.title) : null}
+
+
+ ))}
+
+ );
+ }
+
+ renderToolbar(toolbar: SchemaNode) {
+ const type = (toolbar as Schema).type || (toolbar as string);
+
+ if (type === 'columns-toggler') {
+ this.renderedToolbars.push(type);
+ return this.renderColumnsToggler(toolbar as any);
+ }
+
+ return void 0;
+ }
+
+ // handleAction() {}
+
+ renderActions(region: string) {
+ let {actions, render, store, classnames: cx, data} = this.props;
+
+ actions = Array.isArray(actions) ? actions.concat() : [];
+
+ if (
+ store.toggable &&
+ region === 'header' &&
+ !~this.renderedToolbars.indexOf('columns-toggler')
+ ) {
+ actions.push({
+ type: 'button',
+ children: this.renderColumnsToggler()
+ });
+ }
+
+ return Array.isArray(actions) && actions.length ? (
+
+ {actions.map((action, key) =>
+ render(
+ `action/${key}`,
+ {
+ type: 'button',
+ ...(action as any)
+ },
+ {
+ // onAction: this.handleAction,
+ key
+ // btnDisabled: store.dragging,
+ // data: store.getData(data)
+ }
+ )
+ )}
+
+ ) : null;
+ }
+
+ renderTable() {
+ const {
+ render,
+ title,
+ footer,
+ source,
+ columns,
+ rowSelection,
+ expandable,
+ footSummary,
+ headSummary,
+ classnames: cx,
+ store,
+ ...rest
+ } = this.props;
+
+ let sourceValue = this.props.data.items;
+
+ if (typeof source === 'string') {
+ sourceValue = resolveVariableAndFilter(source, this.props.data, '| raw');
+ }
+
+ if (expandable) {
+ if (expandable.expandableOn) {
+ const expandableOn = cloneDeep(expandable.expandableOn);
+ expandable.rowExpandable = (record: any) => {
+ return evalExpression(expandableOn, record);
+ };
+ delete expandable.expandableOn;
+ }
+
+ if (expandable.type) {
+ expandable.expandedRowRender = (record: any, rowIndex: number) => {
+ return this.renderSchema(expandable, {data: record});
+ };
+ }
+
+ if (expandable.expandedRowClassNameExpr) {
+ const expandedRowClassNameExpr = cloneDeep(expandable.expandedRowClassNameExpr);
+ expandable.expandedRowClassName = (record: any, rowIndex: number) => {
+ return filter(expandedRowClassNameExpr, {record, rowIndex});
+ };
+ delete expandable.expandedRowClassNameExpr;
+ }
+ }
+
+ if (rowSelection) {
+ if (rowSelection.disableOn) {
+ const disableOn = cloneDeep(rowSelection.disableOn);
+
+ rowSelection.getCheckboxProps = (record: any, rowIndex: number) => {
+ return {
+ disabled: evalExpression(disableOn, {record, rowIndex})
+ };
+ };
+
+ delete rowSelection.disableOn;
+ }
+ }
+
+ return ;
+ }
+
+ render() {
+ const {
+ classnames: cx
+ } = this.props;
+
+ this.renderedToolbars = []; // 用来记录哪些 toolbar 已经渲染了
+
+ return
+ {this.renderActions('header')}
+ {this.renderTable()}
+
;
+ }
+}
diff --git a/src/renderers/Table/TableBody.tsx b/src/renderers/Table/TableBody.tsx
index c02b3ea86..f9cd28d19 100644
--- a/src/renderers/Table/TableBody.tsx
+++ b/src/renderers/Table/TableBody.tsx
@@ -156,14 +156,16 @@ export class TableBody extends React.Component {
classnames: cx,
rows,
prefixRowClassName,
- affixRowClassName
+ affixRowClassName,
+ footable
} = this.props;
if (!(Array.isArray(items) && items.length)) {
return null;
}
- const filterColumns = columns.filter(item => item.toggable);
+ // 开启了footable,不需要考虑设置了breakpoint的列了
+ const filterColumns = columns.filter(item => item.toggable && !(footable && item.breakpoint));
const result: any[] = [];
for (let index = 0; index < filterColumns.length; index++) {
@@ -182,14 +184,16 @@ export class TableBody extends React.Component {
}
// 缺少的单元格补齐
+ // 考虑是否设置了
+ // 开启了footable,不需要考虑设置了breakpoint的列了
const appendLen =
- columns.length - result.reduce((p, c) => p + (c.colSpan || 1), 0);
+ (footable ? columns.filter(item => !item.breakpoint).length : columns.length) - result.reduce((p, c) => p + (c.colSpan || 1), 0);
if (appendLen) {
const item = result.pop();
result.push({
...item,
- colSpan: (item.colSpan || 1) + appendLen
+ colSpan: (item?.colSpan || 1) + appendLen
});
}
const ctx = createObject(data, {
diff --git a/src/store/index.ts b/src/store/index.ts
index 957802ff1..c82ac3c36 100644
--- a/src/store/index.ts
+++ b/src/store/index.ts
@@ -12,6 +12,7 @@ import {ComboStore} from './combo';
import {FormStore} from './form';
import {CRUDStore} from './crud';
import {TableStore} from './table';
+import {TableStoreV2} from './table-v2';
import {ListStore} from './list';
import {ModalStore} from './modal';
import {TranslateFn} from '../locale';
@@ -33,6 +34,7 @@ const allowedStoreList = [
ComboStore,
CRUDStore,
TableStore,
+ TableStoreV2,
ListStore,
ModalStore,
FormItemStore,
diff --git a/src/store/table-v2.ts b/src/store/table-v2.ts
new file mode 100644
index 000000000..9eb864617
--- /dev/null
+++ b/src/store/table-v2.ts
@@ -0,0 +1,185 @@
+import {
+ types,
+ getParent,
+ Instance,
+ SnapshotIn,
+ isAlive
+} from 'mobx-state-tree';
+
+import {isVisible, hasVisibleExpression} from '../utils/helper';
+import {iRendererStore} from './iRenderer';
+
+export const Column = types
+ .model('Column', {
+ title: types.optional(types.frozen(), undefined),
+ key: '',
+ toggled: false,
+ breakpoint: types.optional(types.frozen(), undefined),
+ pristine: types.optional(types.frozen(), undefined),
+ toggable: true,
+ index: 0,
+ type: ''
+ })
+ .actions(self => ({
+ toggleToggle() {
+ self.toggled = !self.toggled;
+ const table = getParent(self, 2) as ITableStore;
+
+ if (!table.activeToggaleColumns.length) {
+ self.toggled = true;
+ }
+
+ table.persistSaveToggledColumns();
+ },
+ setToggled(value: boolean) {
+ self.toggled = value;
+ }
+ }));
+
+export type IColumn = Instance;
+export type SColumn = SnapshotIn;
+
+export const Row = types
+ .model('Row', {
+ data: types.frozen({} as any)
+ });
+
+export type IRow = Instance;
+export type SRow = SnapshotIn;
+
+export const TableStoreV2 = iRendererStore
+ .named('TableStoreV2')
+ .props({
+ columns: types.array(Column),
+ rows: types.array(Row),
+ columnsToggable: types.optional(
+ types.union(types.boolean, types.literal('auto')),
+ 'auto'
+ )
+ })
+ .views(self => {
+ function getToggable() {
+ if (self.columnsToggable === 'auto') {
+ return self.columns.filter.length > 10;
+ }
+
+ return self.columnsToggable;
+ }
+
+ function hasColumnHidden() {
+ return self.columns.findIndex(column => !column.toggled) !== -1;
+ }
+
+ function getToggableColumns() {
+ return self.columns.filter(
+ item => isVisible(item.pristine, self.data) && item.toggable !== false
+ );
+ }
+
+ function getActiveToggableColumns() {
+ return getToggableColumns().filter(item => item.toggled);
+ }
+
+ function getFilteredColumns() {
+ return self.columns.filter(
+ item =>
+ item &&
+ isVisible(
+ item.pristine,
+ hasVisibleExpression(item.pristine) ? self.data : {}
+ ) &&
+ (item.toggled || !item.toggable)
+ ).map(item => ({...item.pristine, type: item.type}));
+ }
+
+ return {
+ get toggable() {
+ return getToggable();
+ },
+
+ get columnsData() {
+ return self.columns;
+ },
+
+ get toggableColumns() {
+ return getToggableColumns();
+ },
+
+ get filteredColumns() {
+ return getFilteredColumns();
+ },
+
+ get activeToggaleColumns() {
+ return getActiveToggableColumns();
+ },
+
+ // 是否隐藏了某列
+ hasColumnHidden() {
+ return hasColumnHidden();
+ }
+ }
+ })
+ .actions(self => {
+ function update(config: Partial) {
+ config.columnsToggable !== void 0 &&
+ (self.columnsToggable = config.columnsToggable);
+
+ if (config.columns && Array.isArray(config.columns)) {
+ let columns: Array = config.columns
+ .filter(column => column)
+ .concat();
+
+ columns = columns.map((item, index) => ({
+ ...item,
+ index,
+ type: item.type || 'plain',
+ pristine: item,
+ toggled: item.toggled !== false,
+ breakpoint: item.breakpoint
+ }));
+
+ self.columns.replace(columns as any);
+ }
+ }
+
+ function persistSaveToggledColumns() {
+ const key =
+ location.pathname +
+ self.path +
+ self.toggableColumns.map(item => item.key || item.index).join('-');
+ localStorage.setItem(
+ key,
+ JSON.stringify(self.activeToggaleColumns.map(item => item.index))
+ );
+ }
+
+ return {
+ update,
+ persistSaveToggledColumns,
+
+ // events
+ afterCreate() {
+ setTimeout(() => {
+ if (!isAlive(self)) {
+ return;
+ }
+ const key =
+ location.pathname +
+ self.path +
+ self.toggableColumns.map(item => item.key || item.index).join('-');
+
+ const data = localStorage.getItem(key);
+
+ if (data) {
+ const selectedColumns = JSON.parse(data);
+ self.toggableColumns.forEach(item =>
+ item.setToggled(!!~selectedColumns.indexOf(item.index))
+ );
+ }
+ }, 200);
+ }
+ };
+ });
+
+export type ITableStore = Instance;
+export type STableStore = SnapshotIn;
diff --git a/src/store/table.ts b/src/store/table.ts
index 301331c90..2584e4690 100644
--- a/src/store/table.ts
+++ b/src/store/table.ts
@@ -639,7 +639,8 @@ export const TableStore = iRendererStore
},
get disabledHeadCheckbox() {
- const selectedLength = self.data?.selectedItems.length;
+ // 设置为multiple 默认没选择会报错
+ const selectedLength = self.data?.selectedItems?.length;
const maxLength = self.maxKeepItemSelectionLength;
if (!self.data || !self.keepItemSelectionOnPageChange || !maxLength) {