From 422376dec795d4084785129b0cbc6c0e1d327e84 Mon Sep 17 00:00:00 2001 From: wanglinfang Date: Fri, 24 Dec 2021 11:05:02 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20Table=E5=8E=9F=E5=AD=90=E7=BB=84?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-CN/components/table-v2.md | 901 +++++++++++++++++ examples/components/Components.tsx | 8 + scss/_properties.scss | 4 + scss/components/_table.scss | 168 +++- src/Schema.ts | 1 + src/components/index.tsx | 4 +- src/components/table/Cell.tsx | 76 ++ src/components/table/HeadCellFilter.tsx | 211 ++++ src/components/table/HeadCellSort.tsx | 102 ++ src/components/table/index.tsx | 1192 +++++++++++++++++++++++ src/index.tsx | 1 + src/renderers/QuickEdit.tsx | 5 +- src/renderers/Table-v2/index.tsx | 483 +++++++++ src/renderers/Table/TableBody.tsx | 12 +- src/store/index.ts | 2 + src/store/table-v2.ts | 185 ++++ src/store/table.ts | 3 +- 17 files changed, 3349 insertions(+), 9 deletions(-) create mode 100755 docs/zh-CN/components/table-v2.md create mode 100644 src/components/table/Cell.tsx create mode 100644 src/components/table/HeadCellFilter.tsx create mode 100644 src/components/table/HeadCellSort.tsx create mode 100644 src/components/table/index.tsx create mode 100644 src/renderers/Table-v2/index.tsx create mode 100644 src/store/table-v2.ts 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 ( +
+ + {this.renderTFoot()} +
+
+ ); + } + + 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) { From 34f73bf4cac7a96afca6d09c23a492fac70b4af6 Mon Sep 17 00:00:00 2001 From: wanglinfang Date: Fri, 31 Dec 2021 17:33:11 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=E8=B0=83=E6=95=B4=E5=88=97=E5=AE=BD?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E4=BF=AE=E5=A4=8D=EF=BC=9B=E6=96=B0=E5=A2=9E?= =?UTF-8?q?sticky=E5=B1=9E=E6=80=A7=EF=BC=9B=E5=88=97=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=EF=BC=9B=E5=8D=95=E5=85=83=E6=A0=BC=E6=94=AF=E6=8C=81=E7=BC=96?= =?UTF-8?q?=E8=BE=91=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/zh-CN/components/table-v2.md | 2514 ++++++++++++++++- examples/components/Components.tsx | 15 +- examples/style.scss | 1 + scss/_properties.scss | 21 + scss/components/_table-v2.scss | 912 ++++++ scss/components/_table.scss | 168 +- scss/themes/_common.scss | 1 + scss/themes/_cxd-variables.scss | 9 +- src/Schema.ts | 2 + src/components/table/Cell.tsx | 34 +- src/components/table/HeadCellDropDown.tsx | 115 + src/components/table/HeadCellFilter.tsx | 212 +- src/components/table/HeadCellSelect.tsx | 86 + src/components/table/index.tsx | 1241 +++++--- src/renderers/QuickEdit.tsx | 5 +- .../Table-v2/HeadCellSearchDropdown.tsx | 244 ++ src/renderers/Table-v2/TableCell.tsx | 19 + src/renderers/Table-v2/index.tsx | 821 +++++- .../Table/HeadCellSearchDropdown.tsx | 1 - src/renderers/Table/TableBody.tsx | 12 +- src/store/table-v2.ts | 523 +++- src/store/table.ts | 3 +- 22 files changed, 6085 insertions(+), 874 deletions(-) create mode 100644 scss/components/_table-v2.scss create mode 100644 src/components/table/HeadCellDropDown.tsx create mode 100644 src/components/table/HeadCellSelect.tsx create mode 100644 src/renderers/Table-v2/HeadCellSearchDropdown.tsx create mode 100644 src/renderers/Table-v2/TableCell.tsx diff --git a/docs/zh-CN/components/table-v2.md b/docs/zh-CN/components/table-v2.md index c34090eec..163875211 100755 --- a/docs/zh-CN/components/table-v2.md +++ b/docs/zh-CN/components/table-v2.md @@ -48,7 +48,11 @@ order: 67 } ``` -## 可选择 - 多选 +## 可选择 + +支持单选、多选 + +### 多选 ```schema: scope="body" { @@ -88,7 +92,140 @@ order: 67 } ``` -## 可选择 - 单选 +### 点击整行选择 + +```schema: scope="body" +{ + "type": "service", + "api": "/api/sample?perPage=5", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "rowSelection": { + "type": "checkbox", + "keyField": "id", + "rowClick": true + }, + "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": "checkbox", + "keyField": "id", + "selectedRowKeys": [1, 2] + }, + "columns": [ + { + "title": "ID", + "key": "id" + }, + { + "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": "checkbox", + "keyField": "id", + "selectedRowKeysExpr": "<%= data.record.id === 1 %>" + }, + "columns": [ + { + "title": "ID", + "key": "id" + }, + { + "title": "Engine", + "key": "engine" + }, + { + "title": "Version", + "key": "version" + }, + { + "title": "Browser", + "key": "browser" + }, + { + "title": "Operation", + "key": "operation", + "type": "button", + "label": "删除", + "size": "sm" + } + ] + } + ] +} +``` + +### 单选 + +可通过`disableOn`来控制哪一行不可选,不可选情况下会有禁用样式,但如果行内如果有除文字外的其他组件,禁用样式需要自行控制 ```schema: scope="body" { @@ -103,6 +240,63 @@ order: 67 "keyField": "id", "disableOn": "this.record.id === 1" }, + "columns": [ + { + "title": "Engine", + "key": "engine" + }, + { + "title": "Version", + "key": "version" + }, + { + "title": "Browser", + "key": "browser" + } + ] + } + ] +} +``` + +### 自定义选择菜单 + +内置全选`all`、反选`invert`、清空`none`、选中奇数行`odd`、选中偶数行`even`,存在禁止选择的行,不参与计算奇偶数 + +```schema: scope="body" +{ + "type": "service", + "api": "/api/sample?perPage=5", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "rowSelection": { + "type": "checkbox", + "keyField": "id", + "selections": [ + { + "key": "all", + "text": "全选所有" + }, + { + "key": "invert", + "text": "反选当页" + }, + { + "key": "none", + "text": "清空所有" + }, + { + "key": "odd", + "text": "选择奇数行" + }, + { + "key": "even", + "text": "选择偶数行" + } + ] + }, "columns": [ { "title": "Engine", @@ -253,13 +447,65 @@ order: 67 } ], "expandable": { - "expandableOn": "this.id === 1 || this.id === 3", + "expandableOn": "this.record.id === 1 || this.record.id === 3", "keyField": "id", - "type": "tpl", - "html": "
    测试测试
    ", "expandedRowClassNameExpr": "<%= data.rowIndex % 2 ? 'bg-success' : '' %>", "expandedRowKeys": ["3"] - } + }, + "expandableBody": [ + { + "type": "tpl", + "html": "
    测试测试
    " + } + ] + } + ] +} +``` + +## 已展开 - 正则表达式 + +```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.record.id === 1 || this.record.id === 3", + "keyField": "id", + "expandedRowClassNameExpr": "<%= data.rowIndex % 2 ? 'bg-success' : '' %>", + "expandedRowKeysExpr": "<%= data.record.id == '3' %>" + }, + "expandableBody": [ + { + "type": "tpl", + "html": "
    测试测试
    " + } + ] } ] } @@ -450,8 +696,7 @@ order: 67 }, { "title": "Version", - "key": "version", - "fixed": "left" + "key": "version" }, { "title": "Grade", @@ -479,8 +724,69 @@ order: 67 }, { "title": "Platform", - "key": "platform", - "fixed": "right" + "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": "Version", + "key": "version" + }, + { + "title": "Grade", + "key": "grade" + }, + { + "title": "Grade1", + "key": "grade1", + "children": [ + { + "title": "Browser", + "key": "browser" + } + ] + }, + { + "title": "Platform1", + "key": "platform1", + "children": [ + { + "title": "Badge1", + "key": "badgeText1", + "children": [ + { + "title": "ID", + "key": "id" + }, + { + "title": "Platform", + "key": "platform" + }, + { + "title": "Badge", + "key": "badgeText" + } + ] + } + ] } ] } @@ -490,6 +796,10 @@ order: 67 ## 拖拽排序 +支持手动拖动排序 + +### 默认拖拽排序 + ```schema: scope="body" { "type": "service", @@ -504,11 +814,6 @@ order: 67 "title": "Engine", "key": "engine", }, - { - "title": "Version", - "key": "version", - "fixed": "left" - }, { "title": "Grade", "key": "grade" @@ -529,8 +834,7 @@ order: 67 }, { "title": "Platform", - "key": "platform", - "fixed": "right" + "key": "platform" } ] } @@ -538,7 +842,204 @@ order: 67 } ``` -## 顶部总结栏 +### 嵌套拖拽排序 + +数据源嵌套情况下,仅允许同层级之间排序 + +```schema: scope="body" +{ + "type":"page", + "body":{ + "type":"service", + "data":{ + "rows":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":1, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":1001, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":10001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":10002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":1002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":2, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":2001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":2002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.5", + "platform":"Win 95+", + "version":"5.5", + "grade":"A", + "id":3, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":3001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":3002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 6", + "platform":"Win 98+", + "version":"6", + "grade":"A", + "id":4, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":4001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":4002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 7", + "platform":"Win XP SP2+", + "version":"7", + "grade":"A", + "id":5, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":5001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":5002 + } + ] + } + ] + }, + "body":[ + { + "type":"table-v2", + "source":"$rows", + "columns":[ + { + "key":"engine", + "title":"Engine" + }, + { + "key":"grade", + "title":"Grade" + }, + { + "key":"browser", + "title":"Browser" + }, + { + "key":"id", + "title":"ID" + }, + { + "key":"platform", + "title":"Platform" + } + ], + "keyField":"id", + "draggable": true + } + ] + } +} +``` + +## 总结栏 + +### 顶部单行 ```schema: scope="body" { @@ -595,7 +1096,73 @@ order: 67 } ``` -## 尾部总结栏 +### 顶部多行 + +```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 + } + ], + [ + { + "type": "text", + "text": "总结", + "colSpan": 6 + } + ] + ], + "rowSelection": { + "type": "checkbox", + "keyField": "id" + } + } + ] +} +``` + +### 尾部单行 ```schema: scope="body" { @@ -651,6 +1218,69 @@ order: 67 } ``` +### 尾部多行 + +```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": "总计", + "colSpan": 6 + }, + [ + { + "type": "tpl", + "tpl": "测试测试", + "colSpan": 5 + }, + { + "type": "text", + "text": "总结", + "colSpan": 1 + } + ] + ] + } + ] +} +``` + ## 调整列宽 ```schema: scope="body" @@ -661,7 +1291,6 @@ order: 67 { "type": "table-v2", "source": "$rows", - "bordered": true, "scroll": {"x": 1000}, "resizable": true, "columns": [ @@ -711,9 +1340,8 @@ order: 67 { "type": "table-v2", "source": "$rows", - "columnsToggable": true, + "columnsTogglable": true, "title": "表格的标题", - "bordered": true, "columns": [ { "title": "Engine", @@ -797,7 +1425,7 @@ order: 67 } ``` -## 数据为空 +## 数据加载中 ```schema { @@ -845,17 +1473,1805 @@ order: 67 ## 树形结构 +当行数据中存在 children 属性时,可以自动嵌套显示下去。也可以通过设置 childrenColumnName 进行配置。 + +### 默认嵌套 + +```schema: scope="body" +{ + "type":"page", + "body":{ + "type":"service", + "data":{ + "rows":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":1, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":1001, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":10001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":10002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":1002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":2, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":2001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":2002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.5", + "platform":"Win 95+", + "version":"5.5", + "grade":"A", + "id":3, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":3001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":3002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 6", + "platform":"Win 98+", + "version":"6", + "grade":"A", + "id":4, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":4001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":4002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 7", + "platform":"Win XP SP2+", + "version":"7", + "grade":"A", + "id":5, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":5001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":5002 + } + ] + } + ] + }, + "body":[ + { + "type":"table-v2", + "source":"$rows", + "columns":[ + { + "key":"engine", + "title":"Engine" + }, + { + "key":"grade", + "title":"Grade" + }, + { + "key":"version", + "title":"Version" + }, + { + "key":"browser", + "title":"Browser" + }, + { + "key":"id", + "title":"ID" + }, + { + "key":"platform", + "title":"Platform" + } + ], + "keyField":"id" + } + ] + } +} +``` + +### 多选嵌套 + +表格支持多选的同时支持级联选中 + +```schema: scope="body" +{ + "type":"page", + "body":{ + "type":"service", + "data":{ + "rows":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":1, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":1001, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":10001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":10002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":1002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":2, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":2001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":2002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.5", + "platform":"Win 95+", + "version":"5.5", + "grade":"A", + "id":3, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":3001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":3002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 6", + "platform":"Win 98+", + "version":"6", + "grade":"A", + "id":4, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":4001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":4002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 7", + "platform":"Win XP SP2+", + "version":"7", + "grade":"A", + "id":5, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":5001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":5002 + } + ] + } + ] + }, + "body":[ + { + "type":"table-v2", + "source":"$rows", + "columns":[ + { + "key":"engine", + "title":"Engine" + }, + { + "key":"grade", + "title":"Grade" + }, + { + "key":"version", + "title":"Version" + }, + { + "key":"browser", + "title":"Browser" + }, + { + "key":"id", + "title":"ID" + }, + { + "key":"platform", + "title":"Platform" + } + ], + "keyField":"id", + "rowSelection":{ + "type":"checkbox", + "keyField":"id" + } + } + ] + } +} +``` + +### 单选嵌套 + +单选 不同层级之间都是互斥选择 + +```schema: scope="body" +{ + "type":"page", + "body":{ + "type":"service", + "data":{ + "rows":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":1, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":1001, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":10001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":10002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":1002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":2, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":2001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":2002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.5", + "platform":"Win 95+", + "version":"5.5", + "grade":"A", + "id":3, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":3001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":3002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 6", + "platform":"Win 98+", + "version":"6", + "grade":"A", + "id":4, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":4001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":4002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 7", + "platform":"Win XP SP2+", + "version":"7", + "grade":"A", + "id":5, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":5001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":5002 + } + ] + } + ] + }, + "body":[ + { + "type":"table-v2", + "source":"$rows", + "columns":[ + { + "key":"engine", + "title":"Engine" + }, + { + "key":"grade", + "title":"Grade" + }, + { + "key":"version", + "title":"Version" + }, + { + "key":"browser", + "title":"Browser" + }, + { + "key":"id", + "title":"ID" + }, + { + "key":"platform", + "title":"Platform" + } + ], + "keyField":"id", + "rowSelection":{ + "type":"radio", + "keyField":"id" + } + } + ] + } +} +``` + +### 缩进设置 + +```schema: scope="body" +{ + "type":"page", + "body":{ + "type":"service", + "data":{ + "rows":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":1, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":1001, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":10001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":10002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":1002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":2, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":2001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":2002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.5", + "platform":"Win 95+", + "version":"5.5", + "grade":"A", + "id":3, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":3001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":3002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 6", + "platform":"Win 98+", + "version":"6", + "grade":"A", + "id":4, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":4001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":4002 + } + ] + }, + { + "engine":"Trident", + "browser":"Internet Explorer 7", + "platform":"Win XP SP2+", + "version":"7", + "grade":"A", + "id":5, + "children":[ + { + "engine":"Trident", + "browser":"Internet Explorer 4.0", + "platform":"Win 95+", + "version":"4", + "grade":"X", + "id":5001 + }, + { + "engine":"Trident", + "browser":"Internet Explorer 5.0", + "platform":"Win 95+", + "version":"5", + "grade":"C", + "id":5002 + } + ] + } + ] + }, + "body":[ + { + "type":"table-v2", + "source":"$rows", + "columns":[ + { + "key":"engine", + "title":"Engine" + }, + { + "key":"grade", + "title":"Grade" + }, + { + "key":"version", + "title":"Version" + }, + { + "key":"browser", + "title":"Browser" + }, + { + "key":"id", + "title":"ID" + }, + { + "key":"platform", + "title":"Platform" + } + ], + "keyField":"id", + "rowSelection":{ + "type":"checkbox", + "keyField":"id" + }, + "indentSize": 20 + } + ] + } +} +``` + ## 列搜索 +```schema: scope="body" +{ + "type": "service", + "api": "/api/sample?perPage=6", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "columns": [ + { + "title": "Engine", + "key": "engine", + "width": 200 + }, + { + "title": "Version", + "key": "version", + "width": 200, + "searchable": true + }, + { + "title": "Browser", + "key": "browser", + "width": 200, + "children": [ + { + "title": "Grade", + "key": "grade", + "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", + "title": "表格的标题", + "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" + } + ], + "sticky": true + } + ] +} +``` + ## 表格尺寸 -## 单元格自动省略 +通过设置size属性来控制表格尺寸,支持`large`、`default`、`small`,`default`是中等尺寸 -## 响应式列 +### 最大尺寸 + +`large`是最大尺寸 + +```schema: scope="body" +{ + "type": "service", + "api": "/api/sample?perPage=6", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "size": "large", + "rowSelection": { + "type": "checkbox", + "keyField": "id" + }, + "columns": [ + { + "title": "Engine", + "key": "engine", + "sorter": true, + "tpl": "${engine|truncate:5}" + }, + { + "title": "Version", + "key": "version", + "sorter": true, + "filterMultiple": true, + "filters": [ + { + "text": "Joe", + "value": "Joe" + }, + { + "text": "Jim", + "value": "Jim" + } + ] + }, + { + "type": "tpl", + "title": "Browser", + "key": "browser", + "tpl": "${browser|truncate:5}", + "searchable": true + }, + { + "title": "Badge", + "key": "badgeText" + }, + { + "title": "Platform", + "key": "platform" + } + ], + "footSummary": [ + { + "type": "text", + "text": "总计", + "fixed": "left" + }, + { + "type": "tpl", + "tpl": "测试测试", + "colSpan": 5 + } + ] + } + ] +} +``` + +### 默认尺寸 + +默认尺寸是`default`,即中等尺寸 + +```schema: scope="body" +{ + "type": "service", + "api": "/api/sample?perPage=6", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "rowSelection": { + "type": "checkbox", + "keyField": "id" + }, + "columns": [ + { + "title": "Engine", + "key": "engine", + "sorter": true, + "tpl": "${engine|truncate:5}" + }, + { + "title": "Version", + "key": "version", + "sorter": true, + "filterMultiple": true, + "filters": [ + { + "text": "Joe", + "value": "Joe" + }, + { + "text": "Jim", + "value": "Jim" + } + ] + }, + { + "type": "tpl", + "title": "Browser", + "key": "browser", + "tpl": "${engine|truncate:5}", + "searchable": true + }, + { + "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", + "size": "small", + "rowSelection": { + "type": "checkbox", + "keyField": "id" + }, + "columns": [ + { + "title": "Engine", + "key": "engine", + "sorter": true, + "tpl": "${engine|truncate:5}" + }, + { + "title": "Version", + "key": "version", + "sorter": true, + "filterMultiple": true, + "filters": [ + { + "text": "Joe", + "value": "Joe" + }, + { + "text": "Jim", + "value": "Jim" + } + ] + }, + { + "type": "tpl", + "title": "Browser", + "key": "browser", + "tpl": "${engine|truncate:5}", + "searchable": true + }, + { + "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", + "title": "表格的标题", + "columns": [ + { + "title": "Engine", + "key": "engine", + "width": 200 + }, + { + "title": "Version", + "key": "version", + "copyable": true + }, + { + "title": "Browser", + "key": "browser", + "width": 200 + }, + { + "title": "Badge", + "key": "badgeText" + }, + { + "title": "Platform", + "key": "platform" + } + ] + } + ] +} +``` + +## 弹出框 + +可以给列配置上`popOver`属性,默认会在该列内容区里渲染一个图标,点击会显示弹出框,用于展示内容 + +```schema: scope="body" +{ + "type": "service", + "api": "/api/sample?perPage=6", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "columns": [ + { + "title": "Engine", + "key": "engine" + }, + { + "title": "Version", + "key": "version" + }, + { + "title": "Browser", + "key": "browser", + "copyable": true, + "popOver": { + "body": { + "type": "tpl", + "tpl": "详细信息:${browser}" + } + } + }, + { + "title": "Badge", + "key": "badgeText" + }, + { + "title": "Platform", + "key": "platform" + } + ] + } + ] +} +``` + +也可以设置图标不展示,结合truncate实现内容自动省略,其余可点击/悬浮查看更多 + +```schema: scope="body" +{ + "type": "service", + "api": "/api/sample?perPage=6", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "columns": [ + { + "title": "Engine", + "key": "engine" + }, + { + "title": "Version", + "key": "version" + }, + { + "type": "tpl", + "title": "Browser", + "key": "browser", + "tpl": "${engine|truncate:5}", + "popOver": { + "trigger": "hover", + "position": "left-top", + "showIcon": false, + "body": { + "type": "tpl", + "tpl": "${browser}" + } + } + }, + { + "title": "Badge", + "key": "badgeText" + }, + { + "title": "Platform", + "key": "platform" + } + ] + } + ] +} +``` + +可以给列配置`popOverEnableOn`属性,该属性为表达式,来控制当前行是否启动`popOver`功能 + +```schema: scope="body" +{ + "type": "service", + "api": "/api/sample?perPage=6", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "columns": [ + { + "title": "ID", + "key": "id", + "popOver": { + "body": { + "type": "tpl", + "tpl": "${id}" + } + }, + "popOverEnableOn": "this.id == 1" + }, + { + "title": "Engine", + "key": "engine" + }, + { + "title": "Version", + "key": "version" + }, + { + "title": "Browser", + "key": "browser", + "popOver": { + "body": { + "type": "tpl", + "tpl": "${browser}" + } + } + }, + { + "title": "Badge", + "key": "badgeText" + }, + { + "title": "Platform", + "key": "platform" + } + ] + } + ] +} +``` + +## 行角标 + +通过属性`itemBadge`,可以为表格行配置[角标](./badge),可以使用[数据映射](../../../docs/concepts/data-mapping)为每一行添加特定的 Badge 属性。[`visibleOn`](../../../docs/concepts/expression)属性控制显示的条件,表达式中`this`可以取到行所在上下文的数据,比如行数据中有`badgeText`字段才显示角标,可以设置`"visibleOn": "this.badgeText"` + +```schema: scope="body" +{ + "type": "service", + "body": { + "type": "table-v2", + "source": "${table}", + "syncLocation": false, + "showBadge": true, + "itemBadge": { + "text": "${badgeText}", + "mode": "ribbon", + "position": "top-left", + "level": "${badgeLevel}", + "visibleOn": "this.badgeText" + }, + "columns": [ + { + "key": "id", + "title": "ID", + "searchable": { + "type": "input-text", + "name": "id", + "label": "主键", + "placeholder": "输入id", + "size": "sm", + } + }, + { + "key": "engine", + "title": "Rendering engine" + }, + { + "key": "browser", + "title": "Browser", + "searchable": { + "type": "select", + "name": "browser", + "label": "浏览器", + "placeholder": "选择浏览器", + "size": "sm", + "options": [ + { + "label": "Internet Explorer ", + "value": "ie" + }, + { + "label": "AOL browser", + "value": "aol" + }, + { + "label": "Firefox", + "value": "firefox" + } + ] + } + }, + { + "key": "platform", + "title": "Platform(s)" + }, + { + "key": "version", + "title": "Engine version", + "searchable": { + "type": "input-number", + "name": "version", + "label": "版本号", + "placeholder": "输入版本号", + "size": "sm", + "mode": "horizontal" + } + }, + { + "key": "grade", + "title": "CSS grade" + } + ] + }, + data: { + table: [ + { + "id": 1, + "engine": "Trident", + "browser": "Internet Explorer 4.0", + "platform": "Win 95+", + "version": "4", + "grade": "X", + "badgeText": "默认", + "badgeLevel": "info" + }, + { + "id": 2, + "engine": "Trident", + "browser": "Internet Explorer 5.0", + "platform": "Win 95+", + "version": "5", + "grade": "C", + "badgeText": "危险", + "badgeLevel": "danger" + }, + { + "id": 3, + "engine": "Trident", + "browser": "Internet Explorer 5.5", + "platform": "Win 95+", + "version": "5.5", + "grade": "A" + }, + { + "id": 4, + "engine": "Trident", + "browser": "Internet Explorer 6", + "platform": "Win 98+", + "version": "6", + "grade": "A" + }, + { + "id": 5, + "engine": "Trident", + "browser": "Internet Explorer 7", + "platform": "Win XP SP2+", + "version": "7", + "grade": "A" + } + ] + } +} +``` + +## 表头提示 + +通过设置列属性`remark`,可为每一列的表头增加提示信息。 + +```schema: scope="body" +{ + "type": "page", + "body": { + "type": "service", + "api": "/api/sample?perPage=5", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "columns": [ + { + "title": "Engine", + "key": "engine", + "remark": "表头提示" + }, + { + "title": "Version", + "key": "version" + }, + { + "title": "Browser", + "key": "browser" + }, + { + "title": "Badge", + "key": "badgeText" + }, + { + "title": "Grade", + "key": "grade" + }, + { + "title": "Platform", + "key": "platform" + } + ] + } + ] + } +} +``` + +## 快速编辑 + +可以通过给列配置:`"quickEdit": true`,Table配置:`quickSaveApi`,可以实现表格内快速编辑并批量保存的功能。 + +```schema: scope="body" +{ + "type": "page", + "body": { + "type": "service", + "api": "/api/sample?perPage=5", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "quickSaveApi": { + "url": "/api/mock2/sample/bulkUpdate", + "method": "put" + }, + "columns": [ + { + "title": "Engine", + "key": "engine", + "quickEdit": true + }, + { + "title": "Version", + "key": "version" + }, + { + "title": "Browser", + "key": "browser" + }, + { + "title": "Badge", + "key": "badgeText" + } + ] + } + ] + } +} +``` + +#### 指定编辑表单项类型 + +`quickEdit`也可以配置对象形式,可以指定编辑表单项的类型,例如`"type": "select"`: + +```schema: scope="body" +{ + "type": "page", + "body": { + "type": "service", + "api": "/api/sample?perPage=5", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "quickSaveApi": { + "url": "/api/mock2/sample/bulkUpdate", + "method": "put" + }, + "columns": [ + { + "key": "id", + "title": "ID" + }, + { + "key": "grade", + "title": "CSS grade", + "quickEdit": { + "type": "select", + "options": [ + "A", + "B", + "C", + "D", + "X" + ] + } + } + ] + } + ] + } +} +``` + +#### 快速编辑多个表单项 + +```schema: scope="body" +{ + "type": "page", + "body": { + "type": "service", + "api": "/api/sample?perPage=5", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "quickSaveApi": { + "url": "/api/mock2/sample/bulkUpdate", + "method": "put" + }, + "columns": [ + { + "key": "id", + "title": "ID" + }, + { + "key": "grade", + "title": "CSS grade", + "quickEdit": { + "body": [ + { + "type": "select", + "name": "grade", + "options": [ + "A", + "B", + "C", + "D", + "X" + ] + }, + + { + "label": "id", + "type": "input-text", + "name": "id" + } + ] + } + } + ] + } + ] + } +} +``` + +#### 内联模式 + +配置`quickEdit`的`mode`为`inline`。可以直接将编辑表单项渲染至表格内,可以直接操作编辑。 + +```schema: scope="body" +{ + "type": "page", + "body": { + "type": "service", + "api": "/api/sample?perPage=5", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "quickSaveApi": { + "url": "/api/mock2/sample/bulkUpdate", + "method": "put" + }, + "columns": [ + { + "key": "id", + "title": "ID" + }, + { + "key": "grade", + "title": "CSS grade", + "quickEdit": { + "mode": "inline", + "type": "select", + "size": "xs", + "options": [ + "A", + "B", + "C", + "D", + "X" + ] + } + }, + { + "key": "switch", + "title": "switch", + "quickEdit": { + "mode": "inline", + "type": "switch", + "onText": "开启", + "offText": "关闭" + } + } + ] + } + ] + } +} +``` + +#### 即时保存 + +如果想编辑完表单项之后,不想点击顶部确认按钮来进行保存,而是即时保存当前标记的数据,则需要配置 `quickEdit` 中的 `"saveImmediately": true`,然后配置接口`quickSaveItemApi`,可以直接将编辑表单项渲染至表格内操作。 + +```schema: scope="body" +{ + "type": "page", + "body": { + "type": "service", + "api": "/api/sample?perPage=5", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "quickSaveItemApi": { + "url": "/api/mock2/sample/$id", + "method": "put" + }, + "columns": [ + { + "key": "id", + "title": "ID" + }, + { + "key": "grade", + "title": "CSS grade", + "quickEdit": { + "mode": "inline", + "type": "select", + "size": "xs", + "options": [ + "A", + "B", + "C", + "D", + "X" + ], + "saveImmediately": true + } + }, + { + "key": "switch", + "title": "switch", + "quickEdit": { + "mode": "inline", + "type": "switch", + "onText": "开启", + "offText": "关闭", + "saveImmediately": true + } + } + ] + } + ] + } +} +``` + +你也可以在`saveImmediately`中配置 api,实现即时保存 + +```schema: scope="body" +{ + "type": "page", + "body": { + "type": "service", + "api": "/api/sample?perPage=5", + "body": [ + { + "type": "table-v2", + "source": "$rows", + "columns": [ + { + "key": "id", + "title": "ID" + }, + { + "key": "grade", + "title": "CSS grade", + "quickEdit": { + "mode": "inline", + "type": "select", + "size": "xs", + "options": [ + "A", + "B", + "C", + "D", + "X" + ], + "saveImmediately": { + "api": "/api/mock2/sample/$id" + } + } + }, + { + "key": "grade", + "title": "CSS grade", + "quickEdit": { + "mode": "inline", + "type": "switch", + "onText": "开启", + "offText": "关闭", + "saveImmediately": true + } + } + ] + } + ] + } +} +``` -## Footable ## 属性表 @@ -864,12 +3280,15 @@ order: 67 | type | `string` | | `"type"` 指定为 table 渲染器 | | title | `string` | | 标题 | | source | `string` | `${items}` | 数据源, 绑定当前环境变量 | -| affixHeader | `boolean` | `true` | 是否固定表头 | +| sticky | `boolean` | `false` | 是否粘性头部 +| footer | `string` \| `Schema` | | 表格尾部 | +| loading | `boolean` | | 表格是否加载中 | | 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 类名 | +| placeholder | `string` \| `Schema` | `暂无数据` | 当没数据的时候的文字提示 +| rowSelection | `rowSelection` | | 行相关配置 | +| rowClassNameExpr | `string` | | 行 CSS 类名,支持模版语法 | +| expandable | `Expandable` | | 展开行配置 | +| lineHeight | `large` \| `middle` | | 行高设置 | | footerClassName | `string` | `Action.md-table-footer` | 底部外层 CSS 类名 | | toolbarClassName | `string` | `Action.md-table-toolbar` | 工具栏 CSS 类名 | | columns | `Array` | | 用来设置列信息 | @@ -885,6 +3304,39 @@ order: 67 | itemBadge | [`BadgeSchema`](./badge) | | 行角标配置 | | autoFillHeight | `boolean` | | 内容区域自适应高度 | +## 行配置属性表 + +| 属性名 | 类型 | 默认值 | 说明 | +| ---------- | --------------------------------------------- | ------- | ---------------- | +| type | `checkbox` \| `radio` | `checkbox` | 指定单选还是多选 | +| fixed | `boolean` | | 选择列是否固定,只能左侧固定 | +| keyField | `string` | `key` | 对应数据源的key值,默认是`key`,可指定为`id`、`shortId`等 | +| disableOn | `string` | | 当前行是否可选择条件,要用 [表达式](../../docs/concepts/expression) | +| selections | `selections` | | 自定义筛选菜单,内置`all`(全选)、`invert`(反选)、`none`(取消选择)、`odd`(选择奇数项)、`even`(选择偶数项) | +| selectedRowKeys | `Array` | | 已选择项 | +| selectedRowKeysExpr | `string` | | 已选择项正则表达式 | +| columnWidth | `number` | | 自定义选择列列宽 | +| rowClick | `boolean` | | 单条任意区域选中 | + +### 选择菜单配置属性表 + +| 属性名 | 类型 | 默认值 | 说明 | +| ---------- | --------------------------------------------- | ------- | ---------------- | +| key | `all` \| `invert` \| `none` \| `odd` \| `even` | `all` | 菜单类型,内置全选、反选、取消选择、选择奇数项、选择偶数项 | +| text | `string` | | 自定义菜单项文本 | + +## 展开行配置属性表 + +| 属性名 | 类型 | 默认值 | 说明 | +| ---------- | --------------------------------------------- | ------- | ---------------- | +| expandableOn | `string` | | 指定可展开的行,要用 [表达式](../../docs/concepts/expression) | +| keyField | `string` | `key` | 对应数据源的key值,默认是`key`,可指定为`id`、`shortId`等 | +| disableOn | `string` | | 当前行是否可选择条件,要用 [表达式](../../docs/concepts/expression) | +| selections | `selections` | | 自定义筛选菜单,内置`all`(全选)、`invert`(反选)、`none`(取消选择)、`odd`(选择奇数项)、`even`(选择偶数项) | +| selectedRowKeys | `Array` | | 已选择项 | +| selectedRowKeysExpr | `string` | | 已选择项正则表达式 | +| columnWidth | `number` | | 自定义选择列列宽 | + ## 列配置属性表 | 属性名 | 类型 | 默认值 | 说明 | diff --git a/examples/components/Components.tsx b/examples/components/Components.tsx index 0afa775e5..5cefefd44 100644 --- a/examples/components/Components.tsx +++ b/examples/components/Components.tsx @@ -775,14 +775,13 @@ 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 v2 表格', + // path: '/zh-CN/components/table-v2', + // component: React.lazy(() => + // import('../../docs/zh-CN/components/table-v2.md').then(wrapDoc) + // ) + // }, { label: 'Table View 表格视图', path: '/zh-CN/components/table-view', diff --git a/examples/style.scss b/examples/style.scss index 31a80a316..4a3408cad 100644 --- a/examples/style.scss +++ b/examples/style.scss @@ -1043,6 +1043,7 @@ body.dark { flex-direction: column; flex-grow: 1; // overflow: hidden; + width: calc(100% - 350px); > div { flex: 1; diff --git a/scss/_properties.scss b/scss/_properties.scss index add4f3e3d..30a52aec9 100644 --- a/scss/_properties.scss +++ b/scss/_properties.scss @@ -1309,22 +1309,43 @@ --Table-searchableForm-borderRadius: #{px2rem(4px)}; --TableCell--edge-paddingX: var(--gap-md); + --TableCell--edge-paddingX-default: var(--gap-base); --TableCell-filterBtn--onActive-color: var(--primary); --TableCell-filterBtn-width: #{px2rem(16px)}; --TableCell-filterPopOver-dropDownItem-height: #{px2rem(34px)}; --TableCell-filterPopOver-dropDownItem-padding: 0 #{px2rem(12px)}; + --TableCell-line-height-large: #{px2rem(40px)}; + --TableCell-line-height-middle: #{px2rem(30px)}; --TableCell-height: #{px2rem(40px)}; + --TableCell-height-default: #{px2rem(41px)}; + --TableCell-height-large: #{px2rem(61px)}; + --TableCell-height-small: #{px2rem(33px)}; --TableCell-paddingX: var(--gap-sm); + --TableCell-paddingX-large: var(--gap-base); + --TableCell-paddingX-small: var(--gap-xs); --TableCell-paddingY: calc( (var(--TableCell-height) - var(--Table-fontSize) * var(--Table-lineHeight)) / 2 ); + --TableCell-paddingY-default: calc( + (var(--TableCell-height-default) - var(--Table-fontSize) * var(--Table-lineHeight)) / + 2 + ); + --TableCell-paddingY-large: calc( + (var(--TableCell-height-large) - var(--Table-fontSize) * var(--Table-lineHeight)) / + 2 + ); + --TableCell-paddingY-small: calc( + (var(--TableCell-height-small) - var(--Table-fontSize) * var(--Table-lineHeight)) / + 2 + ); --TableCell-searchBtn--onActive-color: var(--primary); --TableCell-searchBtn-width: #{px2rem(16px)}; --TableCell-sortBtn--default-onActive-opacity: 1; --TableCell-sortBtn--default-opacity: 0; --TableCell-sortBtn--onActive-color: var(--primary); --TableCell-sortBtn-width: #{px2rem(8px)}; + --TableCell-icon-gap: var(--gap-sm); --Table-fixedLeftLast-boxShadow: inset 10px 0 8px -8px #00000026; --Table-fixedRightFirst-boxShadow: inset -10px 0 8px -8px #00000026; diff --git a/scss/components/_table-v2.scss b/scss/components/_table-v2.scss new file mode 100644 index 000000000..78b87fb5c --- /dev/null +++ b/scss/components/_table-v2.scss @@ -0,0 +1,912 @@ +.#{$ns}Table-v2 { + position: relative; + + border-radius: var(--Table-borderRadius); + margin-bottom: var(--gap-md); + + &.#{$ns}Table-bordered { + border-width: var(--Table-borderWidth) var(--Table-borderWidth) 0 var(--Table-borderWidth); + border-style: solid; + border-color: var(--Table-borderColor); + border-collapse: inherit; + + .#{$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-bottom: var(--Table-borderWidth) solid var(--Table-borderColor); + } + + .#{$ns}Table-title { + border-bottom: var(--Table-borderWidth) solid var(--Table-borderColor); + } + } + + &.#{$ns}Table-large { + .#{$ns}Table-table { + > thead > tr { + > th { + padding: var(--TableCell-paddingY-large) var(--TableCell-paddingX-large); + } + } + + > tbody > tr { + > td, + > th { + padding: var(--TableCell-paddingY-large) var(--TableCell-paddingX-large); + } + } + + > tfoot > tr { + > td { + padding: var(--TableCell-paddingY-large) var(--TableCell-paddingX-large); + } + } + } + + .#{$ns}TableCell-filterBtn { + right: calc( + var(--TableCell-paddingX-large) - var(--TableCell-filterBtn-width) / 2 + ); + } + } + + &.#{$ns}Table-small { + .#{$ns}Table-table { + > thead > tr { + > th { + padding: var(--TableCell-paddingY-small) var(--TableCell-paddingX-small); + } + } + + > tbody > tr { + > td, + > th { + padding: var(--TableCell-paddingY-small) var(--TableCell-paddingX-small); + } + } + + > tfoot > tr { + > td { + padding: var(--TableCell-paddingY-small) var(--TableCell-paddingX-small); + } + } + } + + .#{$ns}TableCell-filterBtn { + right: calc( + var(--TableCell-paddingX-small) - var(--TableCell-filterBtn-width) / 2 + ); + } + } + + .#{$ns}Table-title, + .#{$ns}Table-footer { + background: var(--Table-heading-bg); + padding: calc( + ( + var(--Table-heading-height) - var(--Table-fontSize) * + var(--lineHeightBase) + ) / 2 + ) + var(--gap-sm); + } + + .#{$ns}Table-header { + padding: var(--Table-toolbar-marginY) var(--Table-toolbar-marginX); + + &.#{$ns}Table-sticky-holder { + position: sticky; + z-index: 3; + background: var(--Table-bg); + } + } + + .#{$ns}Table-toolbar { + @include clearfix(); + display: flex; + margin: 0 var(--Table-toolbar-marginX) var(--Table-toolbar-marginY); + flex-wrap: wrap; + + .#{$ns}DropDown { + &-menuItem { + height: auto; + + .#{$ns}Checkbox { + display: flex; + align-items: center; + } + } + } + } + + .#{$ns}Table-header + .#{$ns}Table-toolbar { + padding-top: 0; + } + + .#{$ns}Table-content { + min-height: 0.01%; + overflow-x: auto; + transform: translateZ(0); + + th { + position: relative; + } + } + + .#{$ns}Table-table { + width: 100%; + min-width: 100%; + margin-bottom: 0; + font-size: var(--Table-fontSize); + color: var(--Table-color); + background: var(--Table-bg); + border-spacing: 0; + border-collapse: collapse; + border: none; + + & th, + & td { + text-align: left; + } + + & th.text-center, + & td.text-center, + & th[colspan], + & td[colspan] { + text-align: center; + } + + & th.text-right, + & td.text-right { + text-align: right; + } + + > thead > tr { + > th { + background: var(--Table-thead-bg); + padding: var(--TableCell-paddingY-default) var(--TableCell-paddingX); + + &:first-child { + padding-left: var(--TableCell--edge-paddingX-default); + } + + &.#{$ns}Table-cell-last { + padding-right: var(--TableCell--edge-paddingX-default); + } + + &:not(.#{$ns}Table-cell-last) { + border-right: var(--Table-thead-borderWidth) solid + var(--Table-thead-borderColor); + } + + &.#{$ns}Table-row-expand-icon-cell { + border-right: 0; + } + + font-size: var(--Table-thead-fontSize); + color: var(--Table-thead-color); + font-weight: var(--fontWeightNormal); + white-space: nowrap; + + .#{$ns}Remark { + margin-left: var(--gap-xs); + position: relative; + top: 2px; + } + + .#{$ns}Table-head-cell-wrapper { + display: flex; + } + } + } + + > thead > tr:not(:last-child) { + border-bottom: var(--Table-borderWidth) solid var(--Table-borderColor); + } + + > tbody > tr { + position: relative; + border-bottom: var(--Table-borderWidth) solid var(--Table-borderColor); + + &.#{$ns}Table-summary-row { + > td { + background: var(--Table-thead-bg); + } + } + + > th { + background: var(--Table-thead-bg); + color: var(--Table-thead-color); + font-weight: var(--fontWeightNormal); + white-space: nowrap; + border-right: var(--Table-thead-borderWidth) solid + var(--Table-thead-borderColor); + } + + > td, + > th { + padding: var(--TableCell-paddingY-default) var(--TableCell-paddingX); + + &:first-child { + padding-left: var(--TableCell--edge-paddingX-default); + } + + &:last-child { + padding-right: var(--TableCell--edge-paddingX-default); + } + } + + .#{$ns}Table-cell-wrapper-prefix { + display: flex; + + .#{$ns}Table-expandBtn { + margin-right: 5px; + } + } + + .#{$ns}Table-cell-height-large { + height: var(--TableCell-line-height-large); + line-height: var(--TableCell-line-height-large); + overflow: hidden; + } + + .#{$ns}Table-cell-height-middle { + height: var(--TableCell-line-height-middle); + line-height: var(--TableCell-line-height-middle); + overflow: hidden; + } + + @if var(--Table-strip-bg) !=transparent { + background: transparent; + + &.#{$ns}Table-tr--odd { + background: var(--Table-strip-bg); + } + } + + &.#{$ns}Table-tr--hasItemAction:hover { + cursor: pointer; + } + + &:hover, + &.is-hovered { + background: var(--Table-onHover-bg); + border-color: var(--Table-onHover-borderColor); + color: var(--Table-onHover-color); + + & + tr { + border-color: var(--Table-onHover-borderColor); + } + } + + &.is-checked { + background: var(--Table-onChecked-bg); + border-color: var(--Table-onChecked-borderColor); + color: var(--Table-onChecked-color); + + & + tr { + border-color: var(--Table-onChecked-borderColor); + } + } + + &.is-moved, + &.is-modified { + background: var(--Table-onModified-bg); + border-color: var(--Table-onModified-borderColor); + color: var(--Table-onModified-color); + + & + tr { + border-color: var(--Table-onModified-borderColor); + } + } + + &.is-summary { + background: var(--Table-thead-bg); + color: var(--Table-thead-color); + font-weight: var(--fontWeightNormal); + } + + &.bg-light { + @include color-variant($light, 2%, 3%, 3%, 5%); + color: $text-color; + } + + &.bg-dark { + @include color-variant($dark, 5%, 10%, 5%, 10%); + @include font-variant($dark); + } + + &.bg-black { + @include color-variant($black, 5%, 10%, 5%, 10%); + @include font-variant($black); + } + + &.bg-primary { + @include color-variant($primary, 5%, 10%, 5%, 10%); + @include font-variant($primary); + } + + &.bg-success { + @include color-variant($success, 5%, 10%, 5%, 10%); + @include font-variant($success); + } + + &.bg-info { + @include color-variant($info, 5%, 10%, 5%, 10%); + @include font-variant($info); + } + + &.bg-warning { + @include color-variant($warning, 5%, 10%, 5%, 10%); + @include font-variant($warning); + } + + &.bg-danger { + @include color-variant($danger, 5%, 10%, 5%, 10%); + @include font-variant($danger); + } + + &.is-dragging { + opacity: var(--Table-onDragging-opacity); + } + } + + @for $i from 2 through 10 { + tr.#{$ns}Table-tr--#{$i}th.is-expanded { + .#{$ns}Table-expandCell:before { + right: px2rem(7px) + px2rem(-18px) * ($i - 1); + } + } + + tr.#{$ns}Table-tr--#{$i}th { + .#{$ns}Table-expandBtn { + position: relative; + right: -(px2rem(18px)) * ($i - 1); + } + + .#{$ns}Table-expandCell + td { + position: relative; + + &::before { + content: ''; + position: absolute; + width: px2rem(1px); + top: 0; + bottom: 0; + left: px2rem(-8px) + px2rem(18px) * ($i - 2); + height: auto; + background: var(--Table-tree-borderColor); + } + + &::after { + content: ''; + position: absolute; + height: px2rem(1px); + top: 50%; + left: px2rem(-8px) + px2rem(18px) * ($i - 2); + width: px2rem(10px); + background: var(--Table-tree-borderColor); + } + + padding-left: px2rem(18px) * $i - px2rem(18px); + } + } + + tr.#{$ns}Table-tr--#{$i}th.is-expandable { + .#{$ns}Table-expandCell + td { + padding-left: px2rem(18px) * ($i - 1); + } + } + + tr.#{$ns}Table-tr--#{$i}th.is-last:not(.is-expanded) { + .#{$ns}Table-expandCell + td { + &::before { + height: 50%; + bottom: auto; + } + } + } + } + + > thead > tr > th.#{$ns}Table-checkCell, + > tbody > tr > td.#{$ns}Table-checkCell { + border-right: 0; + white-space: nowrap; + + .#{$ns}Checkbox { + margin: 0; + } + } + + > thead > tr > th.#{$ns}Table-expandCell, + > tbody > tr > td.#{$ns}Table-expandCell { + border-right: 0; + width: px2rem(1px); + padding-right: 0; + } + + > thead > tr > th.#{$ns}Table-dragCell, + > tbody > tr > td.#{$ns}Table-dragCell { + border-right: 0; + width: px2rem(1px); + padding-right: 0; + cursor: move; + > svg { + vertical-align: middle; + } + } + + > tbody > tr > td.#{$ns}Table-expandCell { + position: relative; + + @for $i from 1 through 7 { + .#{$ns}Table-divider-#{$i} { + position: absolute; + width: px2rem(1px); + top: 0; + bottom: 0; + height: 100%; + background: var(--Table-tree-borderColor); + right: px2rem(7px) + px2rem(-18px) * ($i - 1); + } + } + } + + > tbody > tr.is-expanded > td.#{$ns}Table-expandCell { + + &::before { + content: ''; + position: absolute; + width: px2rem(1px); + top: 50%; + bottom: 0; + right: px2rem(7px); + height: auto; + background: var(--Table-tree-borderColor); + } + } + + > thead > tr > th.#{$ns}TableCell--sortable { + padding-right: calc( + var(--TableCell-paddingX) + var(--TableCell-sortBtn-width) + ); + position: relative; + } + + > thead > tr > th.#{$ns}TableCell--searchable { + padding-right: calc( + var(--TableCell-paddingX) + var(--TableCell-searchBtn-width) + ); + position: relative; + } + + > thead > tr > th.#{$ns}TableCell--filterable { + padding-right: calc( + var(--TableCell-paddingX) + var(--TableCell-filterBtn-width) + ); + position: relative; + } + + > tbody > tr.#{$ns}Table-row-disabled { + background: var(--TableRow-onDisabled-bg); + color: var(--TableRow-onDisabled-color); + } + + > tbody > tr:not(.#{$ns}Table-row-disabled) > 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; + + .#{$ns}Table-row-indent { + height: 1px; + } + } + + > tbody > tr.#{$ns}Table-expanded-row > td { + background: var(--Table-onHover-bg); + } + + > tfoot > tr { + border-bottom: var(--Table-borderWidth) solid var(--Table-borderColor); + + > td { + padding: var(--TableCell-paddingY-default) var(--TableCell-paddingX); + background: var(--Table-thead-bg); + } + } + } + + .#{$ns}Table-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); + } + } + + > tbody > tr:not(.#{$ns}Table-row-disabled) { + > td.#{$ns}Table-cell-fix-left { + border-right: none; + } + + > td.#{$ns}Table-cell-fix-left:not(.#{$ns}Table-cell-row-hover) { + background: #FFF; + } + } + + > tfoot > tr > td:not(:last-child) { + &.#{$ns}Table-cell-fix-left-last { + border-right: none; + } + } + + > thead > tr > th:not(:last-child):not(:first-child) { + &.#{$ns}Table-cell-fix-left-last { + border-right: none; + } + } + } + } + + &.#{$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); + } + } + + > tbody > tr:not(.#{$ns}Table-row-disabled) { + > td.#{$ns}Table-cell-fix-right { + border-right: none; + } + + > td.#{$ns}Table-cell-fix-right:not(.#{$ns}Table-cell-row-hover) { + background: #FFF; + } + } + } + + &:not(.#{$ns}Table-bordered) { + .#{$ns}Table-table { + > thead > tr > th.#{$ns}Table-cell-fix-right-first-prev { + border-right: none; + } + + > thead > tr > th:not(:last-child) { + &.#{$ns}Table-cell-fix-right-first { + border-right: none; + } + } + } + } + } + + &.#{$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; + } + + .#{$ns}TableCell-sortBtn { + cursor: pointer; + width: var(--TableCell-sortBtn-width); + height: var(--gap-md); + position: static; + display: inline-block; + transform: none; + color: var(--icon-color); + margin-left: var(--TableCell-icon-gap); + + &:hover { + color: var(--icon-onHover-color); + } + + &--up > svg, + &--down > svg, + &--default > svg { + color: inherit; + width: 13px; + height: 13px; + } + + &--up, + &--down, + &--default { + display: none; + z-index: 2; + font-style: normal; + + &.is-active { + display: inline-block; + } + } + + &--default { + &.is-active { + color: var(--text--muted-color); + &:hover { + color: var(--text-color); + } + } + } + &--up, + &--down { + &.is-active { + color: var(--TableCell-sortBtn--onActive-color); + } + } + } + + .#{$ns}TableCell-searchBtn { + cursor: pointer; + position: static; + transform: translateY(-50%); + color: var(--text--muted-color); + margin-left: var(--TableCell-icon-gap); + + svg.icon { + width: 12px; + height: 12px; + } + + &:hover { + color: var(--text-color); + } + &.is-active { + color: var(--TableCell-searchBtn--onActive-color); + } + } + + .#{$ns}TableCell-searchPopOver { + border: none; + min-width: px2rem(320px); + max-width: px2rem(640px); + + .#{$ns}Panel { + margin: 0; + } + } + + .#{$ns}TableCell-filterBtn { + cursor: pointer; + width: var(--TableCell-filterBtn-width); + position: static; + display: inline-block; + transform: none; + color: var(--text--muted-color); + margin-left: var(--TableCell-icon-gap); + + svg.icon { + width: 13px; + height: 13px; + } + + &:hover { + color: var(--text-color); + } + + &.is-active { + color: var(--TableCell-filterBtn--onActive-color); + } + + .#{$ns}Remark { + display: inline; + } + } + + .#{$ns}TableCell-filterPopOver { + border: none; + width: px2rem(160px); + + .#{$ns}DropDown-menu { + margin: 0; + padding: 0; + + .#{$ns}DropDown-divider { + height: var(--TableCell-filterPopOver-dropDownItem-height); + line-height: var(--TableCell-filterPopOver-dropDownItem-height); + padding: var(--TableCell-filterPopOver-dropDownItem-padding); + background: var(--white); + margin: 0; + + &:hover { + background: var(--light); + color: var(--primary); + } + + &.is-selected { + background: var(--light); + color: var(--primary); + } + + .#{$ns}Checkbox { + width: 100%; + margin: 0; + } + } + } + + .#{$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; + } + } + } + + .#{$ns}TableCell-selectionBtn { + cursor: pointer; + margin-left: 4px; + + svg.icon { + transform: rotate(270deg); + font-size: 12px; + } + } + + .#{$ns}TableCell-selectionPopOver { + .#{$ns}DropDown-menu { + margin: 0; + padding: 0; + } + } + + &.#{$ns}Table-expandBtn { + position: relative; + z-index: 1; + color: var(--Table-expandBtn-color); + display: inline-flex; + justify-content: center; + align-items: center; + width: px2rem(14px); + line-height: 1; + height: 16px; + + > svg { + display: inline-block; + text-align: center; + cursor: pointer; + transition: transform ease-in-out var(--animation-duration), + top ease-in-out var(--animation-duration); + position: relative; + transform-origin: 50% 50%; + width: px2rem(10px); + height: px2rem(10px); + top: 0; + } + + &.is-active > svg { + transform: rotate(90deg); + } + + &:hover { + text-decoration: none; + } + } + + .#{$ns}Table-table > tbody > tr:hover .#{$ns}Table-dragBtn, + .#{$ns}Table-table > tbody > tr.is-dragging .#{$ns}Table-dragBtn, + .#{$ns}Table-table > tbody > tr.is-drop-allowed .#{$ns}Table-dragBtn { + visibility: visible; + } + + .fake-hide { + visibility: hidden; + position: absolute; + } + + .#{$ns}Table-badge { + position: absolute; + top: 0; + left: 0; + } +} + +.#{$ns}InputTable-toolbar { + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; +} + +.#{$ns}InputTable-pager { + margin-left: auto; +} + +.#{$ns}OperationField { + margin: px2rem(-3px); + + > .#{$ns}Button, + > .#{$ns}Button--disabled-wrap > .#{$ns}Button { + margin: px2rem(3px); + } + + > .#{$ns}Button--disabled-wrap > .#{$ns}Button--link { + padding: 0; + } + + > .#{$ns}Button--link { + padding: 0; + margin-right: px2rem(10px); + } +} diff --git a/scss/components/_table.scss b/scss/components/_table.scss index 7ee3c73ba..03a91aabe 100644 --- a/scss/components/_table.scss +++ b/scss/components/_table.scss @@ -8,34 +8,6 @@ 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; @@ -127,9 +99,7 @@ } } - &-heading, - &-title, - &-footer { + &-heading { background: var(--Table-heading-bg); padding: calc( ( @@ -261,6 +231,7 @@ background: var(--Table-bg); border-spacing: 0; border-collapse: collapse; + border: var(--Table-borderWidth) solid var(--Table-borderColor); & th, & td { @@ -632,124 +603,6 @@ ); 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 { @@ -805,9 +658,6 @@ color: var(--TableCell-sortBtn--onActive-color); } } - &:not(:last-child) { - right: calc(var(--TableCell-paddingX) - var(--TableCell-sortBtn-width) / 2 + 15px); - } } &Cell-searchBtn { @@ -903,20 +753,6 @@ } } } - - .#{$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/scss/themes/_common.scss b/scss/themes/_common.scss index 38e07eb99..f1824a0e9 100644 --- a/scss/themes/_common.scss +++ b/scss/themes/_common.scss @@ -49,6 +49,7 @@ @import '../components/wizard'; @import '../components/crud'; @import '../components/table'; +@import '../components/table-v2'; @import '../components/column-toggler'; @import '../components/list'; @import '../components/cards'; diff --git a/scss/themes/_cxd-variables.scss b/scss/themes/_cxd-variables.scss index a9144b8e5..cf5b79801 100644 --- a/scss/themes/_cxd-variables.scss +++ b/scss/themes/_cxd-variables.scss @@ -369,13 +369,13 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06), --Table-color: #333; --Table-thead-color: #333; --Table-lineHeight: 20 / 12; - --Table-borderColor: #f5f5f5; + --Table-borderColor: #{$G8}; --Table-tree-borderColor: #{darken(#f5f5f5, 10%)}; - --Table-thead-bg: #f5f5f5; + --Table-thead-bg: #{$G10}; --Table-thead-borderColor: #fff; --Table-thead-iconColor: #999; --Table-strip-bg: transparent; - --Table-onHover-bg: #f5f5f5; + --Table-onHover-bg: #{$B1}; --Table-onHover-bg-rgb: 245, 251, 255; --Table-onHover-borderColor: #eceff8; --Table-onChecked-bg: #f0faff; @@ -391,6 +391,9 @@ $L1: 0px 4px 6px 0px rgba(8, 14, 26, 0.06), --TableCell-filterPopOver-dropDownItem-height: #{px2rem(30px)}; --TableCell-filterPopOver-dropDownItem-padding: 0 #{px2rem(10px)}; + --TableRow-onDisabled-bg: #{$G10}; + --TableRow-onDisabled-color: #{$G6}; + // listControl --ListControl-item-borderWidth: #{px2rem(1px)}; --ListControl-item-borderRadius: #{$R3}; diff --git a/src/Schema.ts b/src/Schema.ts index 1decf0da5..a15bc1c96 100644 --- a/src/Schema.ts +++ b/src/Schema.ts @@ -118,6 +118,7 @@ import {FormControlSchema} from './renderers/Form/Control'; import {TransferPickerControlSchema} from './renderers/Form/TransferPicker'; import {TabsTransferPickerControlSchema} from './renderers/Form/TabsTransferPicker'; import {JSONSchemaEditorControlSchema} from './renderers/Form/JSONSchemaEditor'; +import {TableSchemaV2} from './renderers/Table-v2'; // 每加个类型,这补充一下。 export type SchemaType = @@ -392,6 +393,7 @@ export type SchemaObject = | StatusSchema | SpinnerSchema | TableSchema + | TableSchemaV2 | TabsSchema | TasksSchema | VBoxSchema diff --git a/src/components/table/Cell.tsx b/src/components/table/Cell.tsx index aa2948630..5fa0b06c8 100644 --- a/src/components/table/Cell.tsx +++ b/src/components/table/Cell.tsx @@ -20,12 +20,16 @@ export interface Props extends ThemeProps, LocaleProps { children?: any; tagName?: string; style?: Object; - column?: ColumnProps + column?: ColumnProps; + wrapperComponent: any; + groupId?: string; // 表头分组随机生成的id + depth?: number; // 表头分组 } export class BodyCell extends React.Component { static defaultProps = { fixed: '', + wrapperComponent: 'td', rowSpan: null, colSpan: null }; @@ -38,28 +42,16 @@ export class BodyCell extends React.Component { key, children, className, - tagName, - style, column, + style, + groupId, + depth, + wrapperComponent: Component, 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} @@ -67,8 +59,10 @@ export class BodyCell extends React.Component { [cx(`Table-cell-fix-${fixed}`)] : fixed, [`text-${column?.align}`] : column?.align })} - style={fixed ? {position: 'sticky', zIndex} : {}} - >{children} + style={fixed ? {position: 'sticky', zIndex, ...style} : {...style}} + data-group-id={groupId || null} + data-depth={depth || null} + >{children} ); } } diff --git a/src/components/table/HeadCellDropDown.tsx b/src/components/table/HeadCellDropDown.tsx new file mode 100644 index 000000000..58495adbf --- /dev/null +++ b/src/components/table/HeadCellDropDown.tsx @@ -0,0 +1,115 @@ +/** + * @file table/HeadCellDropDown + * @author fex + */ + +import React from 'react'; +import {findDOMNode} from 'react-dom'; + +import {themeable, ThemeProps} from '../../theme'; +import {LocaleProps, localeable} from '../../locale'; +import Overlay from '../Overlay'; +import PopOver from '../PopOver'; + +export interface FilterPayload { + closeDropdown?: boolean; +} + +export interface FilterDropdownProps { + setSelectedKeys?: (keys: Array | string) => void, + selectedKeys?: Array | string, + confirm: (payload: FilterPayload) => void, + clearFilters?: () => void +} + +export interface Props extends ThemeProps, LocaleProps { + filterIcon: Function | React.ReactNode; // 图标方法 返回ReactNode + className: string; // 图标样式 + layerClassName: string; // 展开层样式 + active: boolean; // 图标是否高亮 + popOverContainer?: () => Element | Text | null; + filterDropdown: (payload: FilterDropdownProps) => JSX.Element | null ; // 菜单内容 + selectedKeys?: Array | string; + setSelectedKeys?: (keys: Array | string) => void; +} + +export interface State { + isOpened: boolean; +} + +export class HeadCellDropDown extends React.Component { + constructor(props: Props) { + super(props); + + this.state = { + isOpened: false + } + + this.openLayer = this.openLayer.bind(this); + this.closeLayer = this.closeLayer.bind(this); + } + + render() { + const {isOpened} = this.state; + const { + popOverContainer, + active, + className, + layerClassName, + filterIcon, + filterDropdown, + classnames: cx, + classPrefix: ns + } = this.props; + + return ( + + + {filterIcon && typeof filterIcon === 'function' + ? filterIcon(active) : (filterIcon || null)} + + { + isOpened ? ( + findDOMNode(this))} + placement="left-bottom-left-top right-bottom-right-top" + target={ + popOverContainer ? () => findDOMNode(this)!.parentNode : null + } + show + > + + {filterDropdown && typeof filterDropdown === 'function' + ? filterDropdown({...this.props, confirm: (payload: FilterPayload) => { + if (!(payload && payload.closeDropdown === false)) { + this.closeLayer(); + } + }}) : (filterDropdown || null)} + + ) + : null + } + + ); + } + + openLayer() { + this.setState({isOpened: true}); + } + + closeLayer() { + this.setState({isOpened: false}); + } +} + +export default themeable(localeable(HeadCellDropDown)); \ No newline at end of file diff --git a/src/components/table/HeadCellFilter.tsx b/src/components/table/HeadCellFilter.tsx index adcb02e4d..d576f88f3 100644 --- a/src/components/table/HeadCellFilter.tsx +++ b/src/components/table/HeadCellFilter.tsx @@ -9,19 +9,17 @@ import isEqual from 'lodash/isEqual'; import {themeable, ThemeProps} from '../../theme'; import {LocaleProps, localeable} from '../../locale'; -import Overlay from '../Overlay'; -import PopOver from '../PopOver'; +import HeadCellDropDown, {FilterDropdownProps, FilterPayload} from './HeadCellDropDown'; 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; + popOverContainer?: () => Element | Text | null; } export interface OptionProps { @@ -33,7 +31,6 @@ export interface OptionProps { export interface State { options: Array; - isOpened: boolean; filteredValue: Array; } @@ -48,17 +45,11 @@ export class HeadCellFilter extends React.Component { 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 @@ -83,7 +74,7 @@ export class HeadCellFilter extends React.Component { } render() { - const {isOpened, options} = this.state; + const {options} = this.state; const { column, popOverContainer, @@ -91,121 +82,112 @@ export class HeadCellFilter extends React.Component { 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 ? ( -
    • { + const {setSelectedKeys, selectedKeys, confirm, clearFilters} = payload + return options && options.length > 0 ? ( +
        + {!column.filterMultiple + ? options.map((option: any, index) => ( +
      • this.handleClick(confirm, setSelectedKeys, [option.value])} + > + {option.text} +
      • + )) + : options.map((option: any, index) => ( +
      • + + this.handleCheck(confirm, setSelectedKeys, e + ? [option.value] : option.value)} + checked={option.selected} > - - -
      • - ) : null} -
      - ) : null} - - ) - : null - } - + {option.text} + +
    • + ))} + {column.filterMultiple ? ( +
    • + + +
    • + ) : null} +
    + ) : null + }, + setSelectedKeys: (keys: Array) => this.setState({filteredValue: keys}) + }; + + return ( + } + active={column.filtered || options && options.some((item: any) => item.selected)} + popOverContainer={popOverContainer ? popOverContainer : () => findDOMNode(this)} + selectedKeys={this.state.filteredValue} + {...filterProps} + > + ); } - openLayer() { - this.setState({isOpened: true}); + handleClick( + confirm: (payload?: FilterPayload) => void, + setSelectedKeys?: (keys?: (string | Array)) => void, + selectedKeys?: Array + ) { + const {onFilter, column} = this.props; + + setSelectedKeys && setSelectedKeys(selectedKeys); + + onFilter && onFilter({[column.key] : selectedKeys}); + confirm(); } - 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) { + handleCheck( + confirm: (payload?: FilterPayload) => void, + setSelectedKeys?: ((keys: (string | Array)) => void | undefined), + selectedKeys?: Array + ) { const filteredValue = this.state.filteredValue; - if (value) { - this.setState({filteredValue: [...filteredValue, value]}); - } else { - this.setState({filteredValue: filteredValue.filter(v => v !== value)}); + // 选中 + if (Array.isArray(selectedKeys)) { + setSelectedKeys && setSelectedKeys([...filteredValue, ...selectedKeys]); + } else { // 取消选中 + setSelectedKeys && setSelectedKeys(filteredValue.filter(v => v !== selectedKeys)); } } - handleConfirmClick() { - const {onQuery, column} = this.props; - onQuery && onQuery({[column.key] : this.state.filteredValue}); - this.closeLayer(); + handleConfirmClick(confirm: (payload?: FilterPayload) => void) { + const {onFilter, column} = this.props; + onFilter && onFilter({[column.key] : this.state.filteredValue}); + confirm(); } - handleCancelClick() { - this.setState({filteredValue: []}); - this.closeLayer(); + handleCancelClick( + confirm: (payload?: FilterPayload) => void, + setSelectedKeys?: ((keys: (string | Array)) => void | undefined) + ) { + setSelectedKeys && setSelectedKeys([]); + confirm(); } } -export default themeable(localeable(HeadCellFilter)); \ No newline at end of file +export default themeable(localeable(HeadCellFilter)); diff --git a/src/components/table/HeadCellSelect.tsx b/src/components/table/HeadCellSelect.tsx new file mode 100644 index 000000000..aca6560f0 --- /dev/null +++ b/src/components/table/HeadCellSelect.tsx @@ -0,0 +1,86 @@ +/** + * @file table/HeadCellSelect + * @author fex + */ + +import React from 'react'; +import {findDOMNode} from 'react-dom'; + +import {themeable, ThemeProps} from '../../theme'; +import {LocaleProps, localeable} from '../../locale'; +import HeadCellDropDown, {FilterPayload} from './HeadCellDropDown'; +import {RowSelectionOptionProps} from './index'; +import {Icon} from '../icons'; + +export interface Props extends ThemeProps, LocaleProps { + selections: Array; + keys: Array | string; + popOverContainer?: () => Element | Text | null; +} + +export interface State { + key: Array | string; +} + +export class HeadCellSelect extends React.Component { + static defaultProps = { + selections: [] + }; + + constructor(props: Props) { + super(props); + + this.state = { + key: '' + } + } + + render() { + const { + selections, + keys: allKeys, + popOverContainer, + classnames: cx, + classPrefix: ns + } = this.props; + + return ( + } + active={false} + popOverContainer={popOverContainer ? popOverContainer : () => findDOMNode(this)} + filterDropdown={({setSelectedKeys, selectedKeys, confirm, clearFilters}) => { + return
      + {selections.map((item, index) => ( +
    • { + item.onSelect && item.onSelect(allKeys); + this.handleClick(confirm, setSelectedKeys, item.key); + }}> + {item.text} +
    • + ))} +
    ; + }} + setSelectedKeys={keys => this.setState({key: keys})} + selectedKeys={this.state.key} + > +
    + ); + } + + handleClick( + confirm: (payload?: FilterPayload) => void, + setSelectedKeys?: (keys?: Array | string) => void | undefined, + selectedKeys?: Array | string + ) { + setSelectedKeys && setSelectedKeys(selectedKeys); + + confirm(); + } +} + +export default themeable(localeable(HeadCellSelect)); \ No newline at end of file diff --git a/src/components/table/index.tsx b/src/components/table/index.tsx index b2bdb110d..59f21c893 100644 --- a/src/components/table/index.tsx +++ b/src/components/table/index.tsx @@ -9,15 +9,20 @@ import findLastIndex from 'lodash/findLastIndex'; import find from 'lodash/find'; import isEqual from 'lodash/isEqual'; import filter from 'lodash/filter'; +import intersection from 'lodash/intersection'; +import cloneDeep from 'lodash/cloneDeep'; import Sortable from 'sortablejs'; import {themeable, ClassNamesFn, ThemeProps} from '../../theme'; import {localeable, LocaleProps} from '../../locale'; -import {isObject} from '../../utils/helper'; +import {isObject, isBreakpoint, guid} from '../../utils/helper'; import {Icon} from '../icons'; import CheckBox from '../Checkbox'; +import Spinner from '../Spinner'; + import HeadCellSort from './HeadCellSort'; import HeadCellFilter from './HeadCellFilter'; +import HeadCellSelect from './HeadCellSelect'; import Cell from './Cell'; export interface ColumnProps { @@ -35,28 +40,41 @@ export interface ColumnProps { filterMultiple?: boolean; // 是否支持多选 filteredValue?: Array; filtered?: boolean; + filterDropdown?: Function | React.ReactNode; // 列筛选 filterDropdown的优先级更高 和filters两者不能并存 align?: string; // left/right/center + breakpoint?: '*' | 'xs' | 'sm' | 'md' | 'lg'; } export interface ThProps extends ColumnProps { rowSpan: number; colSpan: number; + groupId: string; // 随机生成表头分组的id + depth: number; // 表头分组层级 } export interface TdProps extends ColumnProps { rowSpan: number; colSpan: number; + groupId: string; // 随机生成表头分组的id +} + +export interface RowSelectionOptionProps { + key: string; + text: string; + onSelect: Function; } export interface RowSelectionProps { type: string; + rowClick: boolean; // 点击复选框选中还是点击整行选中 fixed: boolean; // 只能固定在左边 selectedRowKeys: Array; keyField?: string; // 默认是key,可自定义 columnWidth?: number; - onChange: Function; - onSelect: Function; - onSelectAll: Function; + selections?: Array; + onChange?: Function; + onSelect?: Function; + onSelectAll?: Function; getCheckboxProps: Function; } @@ -80,8 +98,10 @@ export interface SummaryProps { render: Function | React.ReactNode; } -export interface ExchangeRecord { - [index: number]: number +export interface OnRowProps { + onRowMouseEnter?: Function; + onRowMouseLeave?: Function; + onRowClick?: Function } export interface TableProps extends ThemeProps, LocaleProps { @@ -96,13 +116,23 @@ export interface TableProps extends ThemeProps, LocaleProps { onSort?: Function; expandable?: ExpandableProps; bordered?: boolean; - size?: string; // default | middle | small + size?: string; // large | default | small headSummary?: Function | React.ReactNode | Array>; footSummary?: Function | React.ReactNode | Array>; draggable?: boolean; + onDrag?: Function; resizable?: boolean; // 列宽调整 placeholder?: string | React.ReactNode | Function; // 数据为空展示 - loading?: boolean; // 数据加载中 + loading?: boolean | string | React.ReactNode; // 数据加载中 + sticky?: boolean; // 粘性头部 + onFilter?: Function; // 筛选/过滤函数 + childrenColumnName?: string; // 控制数据源哪一列作为嵌套数据,不想支持,就随便设置一个不存在的值,默认是children + keyField?: string; // 展开嵌套数据的时候,用哪个字段做唯一标识 + indentSize: number; // 树形展示时 设置缩进值 + onRow?: OnRowProps; // 行操作事件 + rowClassName?: Function; + lineHeight?: string; // 可设置large、middle固定高度,不设置则跟随内容 + showHeader?: boolean; // 是否展示表头 } export interface ScrollProps { @@ -112,19 +142,21 @@ export interface ScrollProps { export interface TableState { selectedRowKeys: Array; - selectedRows: Array; dataSource: Array; expandedRowKeys: Array; + colWidths: Array; } function getMaxLevelThRowSpan(columns: Array) { let maxLevel = 0; - columns.forEach(c => { + + Array.isArray(columns) && columns.forEach(c => { const level = getThRowSpan(c); if (maxLevel < level) { maxLevel = level; } }); + return maxLevel; } @@ -141,60 +173,55 @@ function getThColSpan(column: ColumnProps) { return 1; } - return column.children.length; + let childrenLength = 0; + column.children.forEach(item => childrenLength += getThColSpan(item)); + + return childrenLength; } -function getThColumns( +function buildColumns( columns: Array = [], thColumns: Array>, + tdColumns: 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; + id?: string, + fixed?: boolean | string) { + const maxLevel = getMaxLevelThRowSpan(columns); + // 在处理表头时,如果父级column设置了fixed属性,那么所有children保持一致 + Array.isArray(columns) && columns.forEach(column => { + const groupId = id || guid(); + let childMaxLevel = 0; + if (column.children) { + childMaxLevel = getMaxLevelThRowSpan(column.children); } - tds.push(column); - } - }); + const newColumn = { + ...column, + rowSpan: childMaxLevel ? 1 : maxLevel - childMaxLevel + depth, + colSpan: getThColSpan(column), + groupId, + depth + }; + const tdColumn = { + ...column, + groupId + }; + if (fixed) { + newColumn.fixed = fixed; + tdColumn.fixed = fixed; + } + + if (!thColumns[depth]) { + thColumns[depth] = [] + } + thColumns[depth].push(newColumn); + if (column.children && column.children.length > 0) { + buildColumns(column.children, thColumns, tdColumns, depth + 1, groupId, column.fixed); + } + else { + const {children, ...rest} = tdColumn; + tdColumns.push(rest); + } + }); } function isFixedLeftColumn(fixed: boolean | string | undefined) { @@ -205,7 +232,11 @@ function isFixedRightColumn(fixed: boolean | string | undefined) { return fixed === 'right'; } -function getPreviousLeftWidth(doms: HTMLCollection, index: number, columns: Array) { +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)) { @@ -216,7 +247,11 @@ function getPreviousLeftWidth(doms: HTMLCollection, index: number, columns: Arra return width; } -function getAfterRightWidth(doms: HTMLCollection, index: number, columns: Array) { +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)) { @@ -247,43 +282,38 @@ function getSummaryColumns(summary: Array) { return [first, ...last]; } +const DefaultCellWidth = 40; + export class Table extends React.PureComponent { static defaultProps = { title: '', className: '', dataSource: [], - columns: [] + columns: [], + indentSize: 15, + placeholder: '暂无数据', + showHeader: true }; 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.selectedRows = props.rowSelection + ? this.getSelectedRows(props.dataSource, props.rowSelection?.selectedRowKeys) + : []; this.state = { - selectedRowKeys: props.rowSelection ? (props.rowSelection.selectedRowKeys || []) : [], - selectedRows, - dataSource: props.dataSource || [], + selectedRowKeys: props.rowSelection + ? (props.rowSelection.selectedRowKeys.map(key => key) || []) : [], + dataSource: props.dataSource || [], // 为了支持前端搜索 expandedRowKeys: [ ...(props.expandable ? (props.expandable.expandedRowKeys || []) : []), ...props.expandable ? (props.expandable.defaultExpandedRowKeys || []) : [] - ] + ], + colWidths: [] }; - 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); } @@ -291,10 +321,13 @@ export class Table extends React.PureComponent { return findDOMNode(this); } - // 记录顺序调整 - exchangeRecord: ExchangeRecord; - tdColumns: Array; + // 表头配置 thColumns: Array>; + // 表格配置 + tdColumns: Array; + // 表格当前选中行 + selectedRows: Array; + // 拖拽排序 sortable: Sortable; // 记录点击起始横坐标 resizeStart: number; @@ -306,7 +339,32 @@ export class Table extends React.PureComponent { contentDom: React.RefObject = React.createRef(); headerDom: React.RefObject = React.createRef(); bodyDom: React.RefObject = React.createRef(); - footDom: React.RefObject = React.createRef(); + tfootDom: React.RefObject = React.createRef(); + footDom: React.RefObject = React.createRef(); + + getColWidths() { + const childrens = this.tbodyDom.current?.children[0]?.children || []; + const colWidths: Array = new Array(childrens ? childrens.length : 0); + + for (let i = 0; i < childrens.length; i++) { + const child: any = childrens[i]; + colWidths[i] = child ? child.offsetWidth : null + }; + + return colWidths; + } + + getSelectedRows(dataSource: Array, selectedRowKeys: Array) { + const selectedRows: Array = []; + dataSource.forEach(data => { + if (find(selectedRowKeys, + key => key === data[this.getRowSelectionKeyField()])) { + selectedRows.push(data); + } + }); + + return selectedRows; + } updateTableBodyFixed() { const tbodyDom = this.tbodyDom && (this.tbodyDom.current as HTMLElement); @@ -315,25 +373,31 @@ export class Table extends React.PureComponent { this.updateHeadSummaryFixedRow(tbodyDom); } + updateColWidths() { + this.setState({colWidths: this.getColWidths()}, () => { + if (hasFixedColumn(this.props.columns)) { + const theadDom = this.theadDom && (this.theadDom.current as HTMLElement); + const thColumns = this.thColumns; + this.updateTheadFixedRow(theadDom, thColumns); + this.updateTableBodyFixed(); + } + }); + } + 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); + const tfootDom = this.tfootDom && (this.tfootDom.current as HTMLElement); + tfootDom && this.updateFootSummaryFixedRow(tfootDom); } let current = null; @@ -342,10 +406,17 @@ export class Table extends React.PureComponent { 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)); + + // overflow设置为hidden的情况 + const hiddenDomRefs = [this.headerDom, this.footDom]; + hiddenDomRefs.forEach(ref => + ref && ref.current + && ref.current.addEventListener('wheel', this.onWheel.bind(this))); + // 横向同步滚动 + const scrollDomRefs = [this.bodyDom]; + scrollDomRefs.forEach(ref => + ref && ref.current + && ref.current.addEventListener('scroll', this.onTableScroll.bind(this))); } current && this.updateTableDom(current); @@ -356,60 +427,113 @@ export class Table extends React.PureComponent { if (this.props.resizable) { this.theadDom.current?.addEventListener('mouseup', this.onResizeMouseUp.bind(this)); } + + this.updateStickyHeader(); + + this.updateColWidths(); } 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(); - } - }); // 异步加载数据需求再更新一次 + this.setState({dataSource: [...this.props.dataSource]}, () => this.updateColWidths()); // 异步加载数据需求再更新一次 } + // 选择项发生了变化触发 if (!isEqual(prevState.selectedRowKeys, this.state.selectedRowKeys)) { + // 更新保存的已选择行数据 + this.selectedRows = this.getSelectedRows(this.state.dataSource, this.state.selectedRowKeys); + const {rowSelection} = this.props; rowSelection && rowSelection.onChange - && rowSelection.onChange(this.state.selectedRowKeys, this.state.selectedRows); + && rowSelection.onChange(this.state.selectedRowKeys, this.selectedRows); + + this.setState({selectedRowKeys: this.state.selectedRowKeys.filter((key, i, a) => a.indexOf(key) === i)}); } + + // 外部传入的选择项发生了变化 + if (!isEqual(prevProps.rowSelection?.selectedRowKeys, this.props.rowSelection?.selectedRowKeys)) { + if (this.props.rowSelection) { + this.setState({selectedRowKeys: this.props.rowSelection.selectedRowKeys}); + this.selectedRows = this.getSelectedRows(this.state.dataSource, this.state.selectedRowKeys); + } + } + + // 外部传入的展开项发生了变化 + if (!isEqual(prevProps?.expandable?.expandedRowKeys, this.props.expandable?.expandedRowKeys)) { + if (this.props.expandable) { + this.setState({expandedRowKeys: this.props.expandable.expandedRowKeys || []}); + } + } + // 展开行变化时触发 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'])) { + if (find(this.state.expandedRowKeys, key => key == item[keyField || 'key'])) { expandedRows.push(item); } }); onExpandedRowsChange && onExpandedRowsChange(expandedRows); } } + + // sticky属性发生了变化 + if (prevProps.sticky !== this.props.sticky) { + this.updateStickyHeader(); + } } 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)); + + const hiddenDomRefs = [this.headerDom, this.footDom]; + hiddenDomRefs.forEach(ref => + ref && ref.current + && ref.current.removeEventListener('wheel', this.onWheel.bind(this))); + + const scrollDomRefs = [this.bodyDom]; + scrollDomRefs.forEach(ref => + ref && ref.current + && ref.current.removeEventListener('scroll', this.onTableScroll.bind(this))); this.destroyDragging(); } - exchange(fromIndex: number, toIndex: number, item?: any) { - const {scroll, headSummary} = this.props; + exchange(fromIndex: number, toIndex: number, item: any) { + const {scroll, headSummary, onDrag} = this.props; // 如果有头部总结行 fromIndex就会+1 if ((!scroll || scroll && !scroll.y) && headSummary) { fromIndex = fromIndex - 1; } - // 记录下交换顺序 估计会有用 - // 本身sortable就更新视图了 就不要再setState触发试图更新了 会有问题 - this.exchangeRecord[fromIndex] = toIndex; + const index = toIndex - fromIndex; + + const levels = item.getAttribute('row-levels'); + const rowIndex = +item.getAttribute('row-index'); + + const dataSource = cloneDeep(this.state.dataSource); + const levelsArray = levels ? levels.split(',') : []; + const childrenColumnName = this.getChildrenColumnName(); + let data: Array = dataSource; + let i = 0; + while (i < levelsArray.length) { + data = data[levelsArray[i]][childrenColumnName]; + i++; + } + + if (data && data.length > 0) { + const row = cloneDeep(data[rowIndex]); + data.splice(rowIndex, 1); + data.splice(rowIndex + index, 0, row); + } + + // 先通过事件把最新的排序数据提供出去 + // Sortable修改了dom 数据排序后再通过state更新 会有问题 + onDrag && onDrag(dataSource); } initDragging() { @@ -423,9 +547,22 @@ export class Table extends React.PureComponent { handle: `.${cx('Table-dragCell')}`, ghostClass: 'is-dragging', onMove: (e: any) => { - if (e.related && e.related.classList.contains(`${cx('Table-summary-row')}`)) { + const dragged = e.dragged; + const related = e.related; + + if (related && related.classList.contains(`${cx('Table-summary-row')}`)) { return false; } + + const draggedLevels = dragged.getAttribute('row-levels'); + const relatedLevels = related.getAttribute('row-levels'); + + // 嵌套展示 不属于同一层的 不允许拖动 + // 否则涉及到试图的更新,比如子元素都被拖完了 + if (draggedLevels !== relatedLevels) { + return false; + } + return true; }, onEnd: (e: any) => { @@ -434,7 +571,7 @@ export class Table extends React.PureComponent { return; } - this.exchange(e.oldIndex, e.newIndex); + this.exchange(e.oldIndex, e.newIndex, e.item); } } ); @@ -444,6 +581,24 @@ export class Table extends React.PureComponent { this.sortable && this.sortable.destroy(); } + updateStickyHeader() { + if (this.props.sticky) { + // 如果设置了sticky 如果父元素设置了overflow: auto top值还需要考虑padding值 + let parent = this.headerDom?.current?.parentElement; + setTimeout(() => { + while (parent && window.getComputedStyle(parent, null).getPropertyValue('overflow') !== 'auto') { + parent = parent.parentElement; + } + if (parent && window.getComputedStyle(parent, null).getPropertyValue('overflow') === 'auto') { + const paddingTop = window.getComputedStyle(parent, null).getPropertyValue('padding-top'); + if (paddingTop && this.headerDom && this.headerDom.current) { + this.headerDom.current.style.top = '-' + paddingTop; + } + } + }); + } + } + // 更新一个tr下的td的left和class updateFixedRow(row: HTMLElement, columns: Array) { const {classnames: cx} = this.props; @@ -455,7 +610,8 @@ export class Table extends React.PureComponent { 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'; + dom.style.right = i < children.length - 1 + ? getAfterRightWidth(children, i, columns) + 'px' : '0'; } } // 最后一个左fixed的添加样式 @@ -467,6 +623,9 @@ export class Table extends React.PureComponent { let rightIndex = columns.findIndex(column => isFixedRightColumn(column.fixed)); if (rightIndex > -1) { children[rightIndex]?.classList.add(cx('Table-cell-fix-right-first')); + if (rightIndex > 0) { + children[rightIndex - 1]?.classList.add(cx('Table-cell-fix-right-first-prev')); + } } } @@ -532,29 +691,30 @@ export class Table extends React.PureComponent { } } - renderColGroup() { + renderColGroup(colWidths?: Array) { const {rowSelection, classnames: cx, expandable, draggable} = this.props; const tdColumns = this.tdColumns; - const isExpandable = !!expandable; + const isExpandable = this.isExpandableTable(); + const extraCount = this.getExtraColumnCount(); return ( {draggable ? : null} - {!draggable && rowSelection && rowSelection.type + style={{width: DefaultCellWidth + 'px'}}> : null} + {!draggable && rowSelection ? : null} + style={{width: (rowSelection.columnWidth || DefaultCellWidth) + 'px'}}> : null} { !draggable && isExpandable ? : null } {tdColumns.map((data, index) => { - const width = data.width ? +data.width : data.width; + const width = colWidths ? colWidths[index + extraCount] : data.width; return { // 计算横向移动距离 const distance = event.clientX - this.resizeStart; const tdColumns = [...this.tdColumns]; - let index = tdColumns.findIndex(c => c.key === this.resizeKey) + this.getExtraColumnCount(); + let index = tdColumns.findIndex(c => c.key === this.resizeKey) + + this.getExtraColumnCount(); const colGroup = this.tableDom.current?.getElementsByTagName('colgroup')[0]; let currentWidth = 0; @@ -587,10 +748,54 @@ export class Table extends React.PureComponent { const child = colGroup.children[index] as HTMLElement; currentWidth = child.offsetWidth; } + + let newWidth = 0; + if (colGroup) { + let maxDistance = 0; // 最多可以移动的距离 + // 调宽列 + if (distance > 0) { + for (let i = 0; i < colGroup.children.length; i++) { + const child = colGroup.children[i] as HTMLElement; + // 自适应列 保证有一个最小宽度 + // 如果都设置了固定宽度 那一个都拖不动 + if (!this.tdColumns[i].width) { + maxDistance += child.offsetWidth - DefaultCellWidth; + } + } + if (colGroup.children[index]) { + const child = colGroup.children[index] as HTMLElement; + newWidth = currentWidth + Math.min(distance, maxDistance); + child.style.width = newWidth + 'px'; + } + } else { // 缩短列 + const autoColumns = []; + for (let i = 0; i < colGroup.children.length; i++) { + const child = colGroup.children[i] as HTMLElement; + // 自适应列 保证有一个最小宽度 + // 如果都设置了固定宽度 那一个都拖不动 + if (!this.tdColumns[i].width) { + autoColumns.push(child); + } + } + maxDistance = DefaultCellWidth - currentWidth; + if (colGroup.children[index]) { + const child = colGroup.children[index] as HTMLElement; + newWidth = currentWidth + Math.max(distance, maxDistance); + child.style.width = newWidth + 'px'; + } + const gap = Math.abs(Math.max(distance, maxDistance)) / autoColumns.length; + autoColumns.forEach(c => { + c.style.width = c.offsetWidth + gap + 'px'; + }); + } + } const column = find(tdColumns, c => c.key === this.resizeKey); - if (column) { - column.width = currentWidth + distance; + // 只有通过配置设置过的宽度保存到tdColumns + // 自动分配的不保存 + // 这样可以一直调整了 + if (column && column.width && newWidth) { + column.width = newWidth; } this.tdColumns = tdColumns; @@ -613,57 +818,84 @@ export class Table extends React.PureComponent { } = this.props; const thColumns = this.thColumns; - const keyField = rowSelection ? (rowSelection.keyField || 'key') : ''; + // 获取一行最多th个数 + let maxCount = 0; + thColumns.forEach(columns => { + if (columns.length > maxCount) { + maxCount = columns.length; + } + }); + const keyField = this.getRowSelectionKeyField(); const dataList = rowSelection && rowSelection.getCheckboxProps - ? this.state.dataSource.filter(data => { - const props = rowSelection.getCheckboxProps(data); + ? this.state.dataSource.filter((data, index) => { + const props = rowSelection.getCheckboxProps(data, index); return !props.disabled; }) : this.state.dataSource; - - const isExpandable = !!expandable; + + const isExpandable = this.isExpandableTable(); + + let allRowKeys: Array = []; + let allRows: Array = []; + dataList.forEach(data => { + allRowKeys.push(data[keyField]); + allRows.push(data); + if (!expandable && this.hasChildrenRow(data)) { + allRowKeys = [...allRowKeys, ...this.getDataChildrenKeys(data)]; + data[this.getChildrenColumnName()].forEach((item: any) => allRows.push(item)); + } + }); return ( {thColumns.map((data, index) => { - return + return { draggable && index === 0 ? : null } {!draggable && rowSelection && index === 0 ? + fixed={rowSelection.fixed ? 'left': ''} + className={cx('Table-checkCell')}> {rowSelection.type !== 'radio' - ? 0 && this.state.selectedRowKeys.length < dataList.length} + ? [ 0 + && this.state.selectedRowKeys.length < allRowKeys.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])); + changeRows = dataList.filter(data => !this.hasCheckedRows(data)); } else { - changeRows = this.state.selectedRows; + changeRows = this.selectedRows; } - const selectedRows = value ? dataList : []; + const selectedRows = value ? allRows : []; this.setState({ - selectedRowKeys: value ? dataList.map(data => data[keyField]) : [], - selectedRows + selectedRowKeys: value ? allRowKeys : [] }); - rowSelection.onSelectAll && rowSelection.onSelectAll(value, selectedRows, changeRows); - }}> : null + rowSelection.onSelectAll + && rowSelection.onSelectAll(value, selectedRows, changeRows); + }}>, + rowSelection.selections && rowSelection.selections.length > 0 + ? + : null] : null } : null} { !draggable && isExpandable && index === 0 ? : null } {data.map((item, i) => { @@ -687,7 +919,9 @@ export class Table extends React.PureComponent { } let filter = null; - if (item.filters && item.filters.length > 0) { + if (item.filterDropdown) { + filter = item.filterDropdown; + } else if (item.filters && item.filters.length > 0) { filter = ( { ); } - return - {typeof item.title === 'function' ? item.title() : item.title} - {sort} + const children = + {sort} {filter} {resizable ? this.onResizeMouseDown(e, item.key)}> : null}; + onMouseDown={e => this.onResizeMouseDown(e, item.key)}> : null} +
    ; + + return + {typeof item.title === 'function' ? item.title(children) : item.title} + ; }) }; })} @@ -716,40 +958,66 @@ export class Table extends React.PureComponent { ); } - onRowMouseEnter(event: React.ChangeEvent) { - const {classnames: cx} = this.props; + onRowClick(event: React.ChangeEvent, record?: any, rowIndex?: number) { + const {rowSelection, onRow} = this.props; - let parent = event.target; - while (parent.tagName !== 'TR') { - parent = parent.parentNode; + if (rowSelection && rowSelection.type && rowSelection.rowClick) { + const defaultKey = this.getRowSelectionKeyField(); + + const isSelected = !!find(this.state.selectedRowKeys, key => key === record[defaultKey]); + + this.selectedSingleRow(!isSelected, record); } - if (parent) { - for (let i = 0; i < parent.children.length; i++) { - const td = parent.children[i]; - td.classList.add(cx(`Table-cell-row-hover`)); - } + + if (record && onRow) { + onRow.onRowClick && onRow.onRowClick(event, record, rowIndex); } } - onRowMouseLeave(event: React.ChangeEvent) { - const {classnames: cx} = this.props; + onRowMouseEnter(event: React.ChangeEvent, record?: any, rowIndex?: number) { + const {classnames: cx, onRow} = this.props; let parent = event.target; - while (parent.tagName !== 'TR') { - parent = parent.parentNode; + while (parent && parent.tagName !== 'TR') { + parent = parent.parentElement; } + + if (parent && !parent.classList.contains(cx('Table-row-disabled'))) { + for (let i = 0; i < parent.children.length; i++) { + const td = parent.children[i]; + td.classList.add(cx('Table-cell-row-hover')); // 保证有列fixed的时候样式一致 + } + } + + if (record && onRow) { + onRow.onRowMouseEnter && onRow.onRowMouseEnter(event, record, rowIndex); + } + } + + onRowMouseLeave(event: React.ChangeEvent, record?: any, rowIndex?: number) { + const {classnames: cx, onRow} = this.props; + + let parent = event.target; + while (parent && parent.tagName !== 'TR') { + parent = parent.parentElement; + } + if (parent) { for (let i = 0; i < parent.children.length; i++) { const td = parent.children[i]; - td.classList.remove(cx(`Table-cell-row-hover`)); + td.classList.remove(cx('Table-cell-row-hover')); } } + + if (record && onRow) { + onRow.onRowMouseLeave && onRow.onRowMouseLeave(event, record, rowIndex); + } } onExpandRow(data: any) { const {expandedRowKeys} = this.state; const {expandable} = this.props; - const key = data[expandable?.keyField || 'key']; + const key = data[this.getExpandableKeyField()]; this.setState({expandedRowKeys: [...expandedRowKeys, key]}); expandable?.onExpand && expandable?.onExpand(true, data); } @@ -757,181 +1025,348 @@ export class Table extends React.PureComponent { onCollapseRow(data: any) { const {expandedRowKeys} = this.state; const {expandable} = this.props; - const key = data[expandable?.keyField || 'key']; + const key = data[this.getExpandableKeyField()]; // 还是得模糊匹配 否则'3'、3匹配不上 this.setState({expandedRowKeys: expandedRowKeys.filter(k => k != key)}); expandable?.onExpand && expandable?.onExpand(false, data); } - renderTBody() { + getChildrenColumnName() { + const {childrenColumnName} = this.props; + + return childrenColumnName || 'children'; + } + + getRowSelectionKeyField() { + const {rowSelection} = this.props; + + return rowSelection ? (rowSelection.keyField || 'key') : ''; + } + + getExpandableKeyField() { + const {expandable, keyField} = this.props; + + return expandable?.keyField || keyField || 'key'; + } + + hasChildrenRow(data: any) { + const key = this.getChildrenColumnName(); + return data[key] + && Array.isArray(data[key]) && data[key].length > 0; + } + + // 展开和嵌套不能共存 + isExpandableRow(data: any, rowIndex: number) { + const {expandable} = this.props; + + return expandable && expandable.rowExpandable + && expandable.rowExpandable(data, rowIndex); + } + + // 获取当前行数据所有子行的key值 + getDataChildrenKeys(data: any) { + let keys: Array = []; + + if (this.hasChildrenRow(data)) { + const key = this.getChildrenColumnName(); + data[key].forEach((item: any) => + keys = [ + ...keys, + ...this.getDataChildrenKeys(item), + item[this.getRowSelectionKeyField()] + ]); + } + + return keys; + } + + hasCheckedRows(data: any) { + const selectedRowKeys = this.state.selectedRowKeys; + const childrenKeys = this.getDataChildrenKeys(data); + + return intersection(selectedRowKeys, [ + ...childrenKeys, + data[this.getRowSelectionKeyField()] + ]).length > 0; + } + + hasCheckedChildrenRows(data: any) { + const selectedRowKeys = this.state.selectedRowKeys; + const childrenKeys = this.getDataChildrenKeys(data); + const length = intersection(selectedRowKeys, childrenKeys).length; + + return length > 0; + } + + getExpandedIcons(isExpanded: boolean, record: any) { + const {classnames: cx} = this.props; + + return isExpanded + ? + + + : + + ; + } + + selectedSingleRow(value: boolean, data: any) { + const {rowSelection} = this.props; + + const defaultKey = this.getRowSelectionKeyField(); + const isRadio = rowSelection && rowSelection.type === 'radio'; + + const callback = () => { + rowSelection && rowSelection.onSelect + && rowSelection.onSelect( + data, + value, + this.selectedRows, + this.state.selectedRowKeys + ); + }; + + if (value) { + if (isRadio) { + this.setState({ + selectedRowKeys: [data[defaultKey]] + }, callback); + } else { + this.setState(prevState => ( + { + selectedRowKeys: [ + ...prevState.selectedRowKeys, + data[defaultKey], + ...this.getDataChildrenKeys(data) + ].filter((key, i, a) => a.indexOf(key) === i) + } + ), callback); + } + } else { + if (!isRadio) { + this.setState({ + selectedRowKeys: this.state.selectedRowKeys.filter(key => + ![data[defaultKey], ...this.getDataChildrenKeys(data)].includes(key)) + }, callback); + } + } + } + + renderRow(data: any, rowIndex: number, levels: Array) { const { classnames: cx, rowSelection, expandable, - headSummary, - scroll, draggable, - placeholder + indentSize, + rowClassName, + lineHeight // 是否设置了固定行高 + } = this.props; + + const tdColumns = this.tdColumns; + const isExpandable = this.isExpandableTable(); + const defaultKey = this.getRowSelectionKeyField(); + const colCount = this.getExtraColumnCount(); + + // 当前行是否可展开 + const isExpandableRow = this.isExpandableRow(data, rowIndex); + // 当前行是否有children + const hasChildrenRow = this.hasChildrenRow(data); + + const isExpanded = !!find(this.state.expandedRowKeys, + key => key == data[this.getExpandableKeyField()]); // == 匹配 否则'3'、3匹配不上 + + // 设置缩进效果 + const indentDom = levels.length > 0 ? : null; + + const cells = tdColumns.map((item, i) => { + const render = item.render && typeof item.render === 'function' + ? item.render(data[item.key], data, rowIndex, 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 && isExpandableRow && hasChildrenRow) { + props.rowSpan === 1; + } + } + return props.rowSpan === 0 || props.colSpan === 0 ? null : +
    + {i === 0 && levels.length > 0 ? indentDom : null} + {i === 0 && hasChildrenRow + ? this.getExpandedIcons(isExpanded, data) : null} + {render + ? children + : data[item.key]} +
    +
    ; + }); + + const rowClassNameClass = rowClassName && typeof rowClassName === 'function' + ? rowClassName(data, rowIndex) : ''; + + // 可展开和嵌套不能同时支持 + // 设置了expandable 数据源里有children也就不生效了 + // 拖拽排序 可选、可展开都先不支持了,可以支持嵌套展示 + const checkboxProps = rowSelection && rowSelection.getCheckboxProps + ? rowSelection.getCheckboxProps(data, rowIndex) : {}; + + const expandedRowClassName = expandable && expandable.expandedRowClassName + && typeof expandable.expandedRowClassName === 'function' + ? expandable.expandedRowClassName(data, rowIndex) : ''; + const dataKey = this.getChildrenColumnName(); + + const children = !draggable && isExpandableRow && isExpanded + ? + + {expandable && expandable.expandedRowRender + && typeof expandable.expandedRowRender === 'function' + ? expandable.expandedRowRender(data, rowIndex) : null} + + : (this.hasChildrenRow(data) && isExpanded ? data[dataKey].map((item: any, index: number) => { + return this.renderRow(item, index, [...levels, rowIndex]); + }) : null); + + const isChecked = !!find(this.state.selectedRowKeys, key => key === data[defaultKey]); + const hasChildrenChecked = this.hasCheckedChildrenRows(data); + + return [ this.onRowMouseEnter(e, data, rowIndex)} + onMouseLeave={e => this.onRowMouseLeave(e, data, rowIndex)} + onClick={e => this.onRowClick(e, data, rowIndex)}> + { + draggable ? + + : null + } + {!draggable && rowSelection + ? ( + { + if (!(rowSelection && rowSelection.rowClick)) { + this.selectedSingleRow(value, data); + } + + event && event.stopPropagation(); + } + } + {...checkboxProps}>) : null} + { + !draggable && isExpandable ? + {isExpandableRow || hasChildrenRow + ? this.getExpandedIcons(isExpanded, data) : null} + : null + } + {cells}, children]; + } + + renderTBody() { + const { + classnames: cx, + headSummary, + scroll, + placeholder, + sticky } = 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]; - }) - } - + + {!hasScrollY && !sticky && headSummary + ? this.renderSummaryRow(headSummary) : null} + { + !this.state.dataSource.length + ? + +
    + {typeof placeholder === 'function' ? placeholder() : placeholder} +
    +
    + + : this.state.dataSource.map((data, index) => { + return this.renderRow(data, index, []); + }) + } + ); } + isExpandableTable() { + const {expandable} = this.props; + + // 设置了expandable 优先级更高 + // 就不支持默认嵌套了 + return !!expandable; + } + + isNestedTable() { + const {dataSource} = this.props; + return !!find(dataSource, item => this.hasChildrenRow(item)); + } + getExtraColumnCount() { - const {draggable, expandable, rowSelection} = this.props; + const { + draggable, + rowSelection + } = this.props; + let count = 0; if (draggable) { count++; } else { - if (expandable) { + if (this.isExpandableTable()) { count++; } if (rowSelection) { count++; } } + return count; } @@ -941,52 +1376,49 @@ export class Table extends React.PureComponent { 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} - ) - ) + (Array.isArray(summary) ? summary.forEach((s, index) => { + Array.isArray(s) ? trs.push( this.onRowMouseEnter(e)} + onMouseLeave={e => this.onRowMouseLeave(e)} + key={'summary-tr-' + index} + className={cx('Table-summary-row')}> + {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 + ? summary(dataSource) : [cells.length > 0 ? this.onRowMouseEnter(e)} + onMouseLeave={e => this.onRowMouseLeave(e)} + key="summary-row" + className={cx('Table-summary-row')}>{cells} : null, ...trs]) : null ); } renderTFoot() { const {classnames: cx, footSummary} = this.props; - return ( + return ( {this.renderSummaryRow(footSummary)} ); } - updateTableDom(dom: HTMLDivElement) { + updateTableDom(dom: HTMLElement) { const {classnames: cx} = this.props; const {scrollLeft, scrollWidth, offsetWidth} = dom; const table = this.tableDom.current; @@ -1010,22 +1442,42 @@ export class Table extends React.PureComponent { 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; + onWheel(event: WheelEvent) { + const {currentTarget, deltaX} = event as unknown as React.WheelEvent; + if (deltaX) { + this.onTableScroll({ + target: currentTarget, + scrollLeft: currentTarget.scrollLeft + deltaX + }); + + event.preventDefault(); } + } + + onTableScroll(event: {target: HTMLDivElement, scrollLeft?: number}) { + const scrollDomRefs = [ + this.headerDom, + this.bodyDom, + this.footDom + ]; + + const {target, scrollLeft} = event; + + scrollDomRefs.forEach(ref => { + const current = ref && ref.current + if (current && current !== target) { + current.scrollLeft = (scrollLeft || target.scrollLeft); + } + }); this.updateTableDom(target); } renderLoading() { - const {classnames: cx} = this.props; - return
    加载中
    ; + const {classnames: cx, loading} = this.props; + return
    { + typeof loading === 'boolean' ? : loading + }
    ; } renderTable() { @@ -1033,6 +1485,7 @@ export class Table extends React.PureComponent { scroll, footSummary, loading, + showHeader, classnames: cx } = this.props; @@ -1050,7 +1503,7 @@ export class Table extends React.PureComponent { : {tableLayout: 'auto'}} className={cx('Table-table')}> {this.renderColGroup()} - {this.renderTHead()} + {showHeader ? this.renderTHead() : null} {!loading ? this.renderTBody() : null} {!loading && footSummary ? this.renderTFoot() : null} @@ -1063,19 +1516,36 @@ export class Table extends React.PureComponent { const { scroll, headSummary, + sticky, + showHeader, classnames: cx } = this.props; + const style = {overflow: 'hidden'}; + if (!!sticky) { + Object.assign(style, {top: 0}); + } + + const tableStyle = {}; + if (scroll && (scroll.y || scroll.x)) { + Object.assign(tableStyle, { + width: (scroll && scroll.x ? (scroll.x + 'px') : '100%'), + tableLayout: 'fixed' + }); + } + return (
    + className={cx('Table-header', { + [cx('Table-sticky-holder')]: !!sticky + })} + style={style}> - {this.renderColGroup()} - {this.renderTHead()} + style={tableStyle}> + {this.renderColGroup(this.state.colWidths)} + {showHeader ? this.renderTHead() : null} {headSummary ? {this.renderSummaryRow(headSummary)} : null}
    @@ -1088,14 +1558,28 @@ export class Table extends React.PureComponent { classnames: cx } = this.props; + const style = {}; + const tableStyle = {}; + if (scroll && (scroll.y || scroll.x)) { + Object.assign(style, { + overflow: 'auto scroll', + maxHeight: scroll.y + }); + + Object.assign(tableStyle, { + width: scroll && scroll.x ? (scroll.x + 'px') : '100%', + tableLayout: 'fixed' + }); + } + return (
    + style={style}> + style={tableStyle}> {this.renderColGroup()} {this.renderTBody()}
    @@ -1111,6 +1595,7 @@ export class Table extends React.PureComponent { return (
    { bordered, resizable, columns, + sticky, classnames: cx } = this.props; + // 过滤掉设置了breakpoint属性的列 + const filterColumns = columns.filter(item => !item.breakpoint || + !isBreakpoint(item.breakpoint)); + this.thColumns = []; this.tdColumns = []; - getThColumns(columns, this.thColumns); - getTdColumns(columns, this.tdColumns); + buildColumns(filterColumns, this.thColumns, this.tdColumns); // 是否设置了纵向滚动 const hasScrollY = scroll && scroll.y; @@ -1166,7 +1655,7 @@ export class Table extends React.PureComponent { return (
    { })}> {title ?
    { typeof title === 'function' ? title() : title - }
    : ''} + }
    : null} - {hasScrollY ? this.renderScrollTable() + {hasScrollY || sticky ? this.renderScrollTable() :
    {this.renderTable()}
    } {footer ?
    { typeof footer === 'function' ? footer() : footer - }
    : ''} + } : null} ); } diff --git a/src/renderers/QuickEdit.tsx b/src/renderers/QuickEdit.tsx index 7a3857d0b..a21801afe 100644 --- a/src/renderers/QuickEdit.tsx +++ b/src/renderers/QuickEdit.tsx @@ -322,14 +322,11 @@ export const HocQuickEdit = ); } - openQuickEdit(e) { + openQuickEdit() { currentOpened = this; this.setState({ isOpened: true }); - // QuickEdit在table中使用时,如果table配置了checkOnItemClick,会同时触发行选中 - // 所以这里阻止冒泡一下 - e.stopPropagation && e.stopPropagation(); } closeQuickEdit() { diff --git a/src/renderers/Table-v2/HeadCellSearchDropdown.tsx b/src/renderers/Table-v2/HeadCellSearchDropdown.tsx new file mode 100644 index 000000000..5cb21cf4a --- /dev/null +++ b/src/renderers/Table-v2/HeadCellSearchDropdown.tsx @@ -0,0 +1,244 @@ +import React from 'react'; +import {findDOMNode} from 'react-dom'; + +import {RendererProps} from '../../factory'; +import {Action} from '../../types'; +import {Icon} from '../../components/icons'; +import {setVariable} from '../../utils/helper'; +import {ITableStore} from '../../store/table-v2'; +import HeadCellDropDown from '../../components/table/HeadCellDropDown'; + +export interface QuickSearchConfig { + type?: string; + controls?: any; + tabs?: any; + fieldSet?: any; + [propName: string]: any; +} + +export interface HeadCellSearchProps extends RendererProps { + name: string; + searchable: boolean | QuickSearchConfig; + classPrefix: string; + onFilter?: (values: object) => void; + onAction?: Function; + store: ITableStore; +} + +export class HeadCellSearchDropDown extends React.Component< + HeadCellSearchProps, + any +> { + + formItems: Array = []; + constructor(props: HeadCellSearchProps) { + super(props); + + this.handleSubmit = this.handleSubmit.bind(this); + this.handleAction = this.handleAction.bind(this); + } + + buildSchema() { + const {searchable, sortable, name, label, translate: __} = this.props; + + let schema: any; + + if (searchable === true) { + schema = { + title: '', + controls: [ + { + type: 'text', + name, + placeholder: label, + clearable: true + } + ] + }; + } else if (searchable) { + if (searchable.controls || searchable.tabs || searchable.fieldSet) { + schema = { + title: '', + ...searchable, + controls: Array.isArray(searchable.controls) + ? searchable.controls.concat() + : undefined + }; + } else { + schema = { + title: '', + className: searchable.formClassName, + controls: [ + { + type: searchable.type || 'text', + name: searchable.name || name, + placeholder: label, + ...searchable + } + ] + }; + } + } + + if (schema && schema.controls && sortable) { + schema.controls.unshift( + { + type: 'hidden', + name: 'orderBy', + value: name + }, + { + type: 'button-group', + name: 'order', + label: __('sort'), + options: [ + { + label: __('asc'), + value: 'asc' + }, + { + label: __('desc'), + value: 'desc' + } + ] + } + ); + } + + if (schema) { + const formItems: Array = []; + schema.controls?.forEach( + (item: any) => + item.name && + item.name !== 'orderBy' && + item.name !== 'order' && + formItems.push(item.name) + ); + this.formItems = formItems; + schema = { + ...schema, + type: 'form', + wrapperComponent: 'div', + actions: [ + { + type: 'button', + label: __('reset'), + actionType: 'clear-and-submit' + }, + + { + type: 'button', + label: __('cancel'), + actionType: 'cancel' + }, + + { + label: __('search'), + type: 'submit', + primary: true + } + ] + }; + } + + return schema || 'error'; + } + + handleAction(e: any, action: Action, ctx: object, confirm: Function) { + const {onAction} = this.props; + + if (action.actionType === 'cancel' || action.actionType === 'close') { + confirm(); + return; + } + + if (action.actionType === 'reset') { + confirm(); + this.handleReset(); + return; + } + + onAction && onAction(e, action, ctx); + } + + handleReset() { + const {onFilter, data, name, store} = this.props; + const values = {...data}; + this.formItems.forEach(key => setVariable(values, key, undefined)); + + if (values.orderBy === name) { + values.orderBy = ''; + values.order = 'asc'; + } + + store.updateQuery(values); + + onFilter && onFilter(values); + } + + handleSubmit(values: any, confirm: Function) { + const {onFilter, name, store} = this.props; + + if (values.order) { + values = { + ...values, + orderBy: name + }; + } + + store.updateQuery(values); + + onFilter && onFilter(values); + + confirm(); + } + + isActive() { + const {data, name, orderBy} = this.props; + + return (orderBy && orderBy === name) || this.formItems.some(key => data?.[key]); + } + + render() { + const { + render, + name, + data, + searchable, + store, + orderBy, + popOverContainer, + classPrefix: ns, + classnames: cx + } = this.props; + + const formSchema = this.buildSchema(); + const isActive = this.isActive(); + + return ( + } + popOverContainer={popOverContainer ? popOverContainer : () => findDOMNode(this)} + filterDropdown={({setSelectedKeys, selectedKeys, confirm, clearFilters}) => { + return render('quick-search-form', formSchema, { + data: { + ...data, + orderBy, + order: orderBy && orderBy === name ? (store as ITableStore).order : '' + }, + onSubmit: (values: object) => this.handleSubmit(values, confirm), + onAction: (e: any, action: Action, ctx: object) => { + this.handleAction(e, action, ctx, confirm); + } + }) as JSX.Element; + }}> + + ); + } +} diff --git a/src/renderers/Table-v2/TableCell.tsx b/src/renderers/Table-v2/TableCell.tsx new file mode 100644 index 000000000..037c40545 --- /dev/null +++ b/src/renderers/Table-v2/TableCell.tsx @@ -0,0 +1,19 @@ +import {Renderer} from '../../factory'; +import {TableCell} from '../Table'; +import QuickEdit from '../QuickEdit'; +import Copyable from '../Copyable'; +import PopOverable from '../PopOver'; + +@Renderer({ + type: 'cell-field', + name: 'cell-field' +}) +@PopOverable() +@Copyable() +@QuickEdit() +export class CellFieldRenderer extends TableCell { + static defaultProps = { + ...TableCell.defaultProps, + wrapperComponent: 'div' + }; +} diff --git a/src/renderers/Table-v2/index.tsx b/src/renderers/Table-v2/index.tsx index d98e7c2da..a33f6b0dc 100644 --- a/src/renderers/Table-v2/index.tsx +++ b/src/renderers/Table-v2/index.tsx @@ -1,23 +1,38 @@ import React from 'react'; +import {findDOMNode} from 'react-dom'; +import {isAlive} from 'mobx-state-tree'; import cloneDeep from 'lodash/cloneDeep'; +import isEqual from 'lodash/isEqual'; +import {IScopedContext} from '../../Scoped'; import {Renderer, RendererProps} from '../../factory'; -import {SchemaNode, Schema} from '../../types'; -import Table, {ColumnProps} from '../../components/table'; +import {Action} from '../../types'; +import Table from '../../components/table'; import { BaseSchema, SchemaObject, SchemaTokenizeableString } from '../../Schema'; -import {isObject} from '../../utils/helper'; import { - resolveVariableAndFilter + isObject, + anyChanged, + difference, + createObject +} from '../../utils/helper'; +import { + resolveVariableAndFilter, + isPureVariable, + resolveVariable } from '../../utils/tpl-builtin'; import {evalExpression, filter} from '../../utils/tpl'; -import {Icon} from '../../components/icons'; +import {isEffectiveApi} from '../../utils/api'; import Checkbox from '../../components/Checkbox'; -import {TableStoreV2, ITableStore, IColumn} from '../../store/table-v2'; -import ColumnToggler from '../Table/ColumnToggler'; +import {BadgeSchema} from '../../components/Badge'; +import {TableStoreV2, ITableStore, IColumn, IRow} from '../../store/table-v2'; + +import ColumnToggler, {ColumnTogglerProps} from '../Table/ColumnToggler'; +import {HeadCellSearchDropDown} from './HeadCellSearchDropdown'; +import './TableCell'; /** * Table 表格v2渲染器。 @@ -65,7 +80,54 @@ export interface ColumnSchema { /** * 表头分组 */ - children?: Array + children?: Array; + + /** + * 可复制 + */ + copyable?: boolean; + + /** + * 列表头提示 + */ + remark?: string; + + /** + * 快速搜索 + */ + searchable?: boolean | SchemaObject; + + /** + * 快速排序 + */ + sorter?: boolean; + + /** + * 内容居左、居中、居右 + */ + align?: string; + + /** + * 是否固定在左侧/右侧 + */ + fixed?: boolean | string; + + /** + * 当前列是否展示 + */ + toggled?: boolean; +} + +export interface RowSelectionOptionsSchema { + /** + * 选择类型 选择全部 + */ + key: string; // 选择类型 目前只支持all、invert、none、odd、even + + /** + * 选项显示文本 + */ + text: string; } export interface RowSelectionSchema { @@ -77,20 +139,40 @@ export interface RowSelectionSchema { /** * 对应数据源的key值 */ - keyField: string; + keyField?: string; /** * 行是否禁用表达式 */ - disableOn: string; + disableOn?: string; + + /** + * 自定义选择菜单 + */ + selections?: Array; + + /** + * 已选择的key值 + */ + selectedRowKeys?: Array; + + /** + * 已选择的key值表达式 + */ + selectedRowKeysExpr: string; + + /** + * 已选择的key值表达式 + */ + columnWidth?: number; + + /** + * 是否点击行触发选中或取消选中 + */ + rowClick?: boolean; } export interface ExpandableSchema { - /** - * 渲染器类型 - */ - type: string; - /** * 对应数据源的key值 */ @@ -105,9 +187,19 @@ export interface ExpandableSchema { * 展开行自定义样式表达式 */ expandedRowClassNameExpr: string; + + /** + * 已展开的key值 + */ + expandedRowKeys: Array; + + /** + * 已展开的key值表达式 + */ + expandedRowKeysExpr: string; } -export interface TableSchema extends BaseSchema { +export interface TableSchemaV2 extends BaseSchema { /** * 指定为表格类型 */ @@ -116,7 +208,7 @@ export interface TableSchema extends BaseSchema { /** * 表格标题 */ - title: string | SchemaObject; + title?: string | SchemaObject | Array; /** * 表格数据源 @@ -126,7 +218,12 @@ export interface TableSchema extends BaseSchema { /** * 表格可自定义列 */ - columnsToggable: boolean; + columnsTogglable: boolean; + + /** + * 表格可自定义列的一些配置 + */ + columnsToggler: ColumnTogglerProps; /** * 表格列配置 @@ -136,12 +233,77 @@ export interface TableSchema extends BaseSchema { /** * 表格可选择配置 */ - rowSelection: RowSelectionSchema; + rowSelection?: RowSelectionSchema; /** * 表格行可展开配置 */ - expandable: ExpandableSchema; + expandable?: ExpandableSchema; + + /** + * 表格行可展开内容配置 + */ + expandableBody?: Array; + + /** + * 粘性头部 + */ + sticky?: boolean; + + /** + * 加载中 + */ + loading?: boolean | string | SchemaObject; + + /** + * 行角标 + */ + itemBadge?: BadgeSchema; + + /** + * 指定挂载dom + */ + popOverContainer?: any; + + /** + * 嵌套展开记录的唯一标识 + */ + keyField?: string; + + /** + * 数据源嵌套自定义字段名 + */ + childrenColumnName?: string; + + /** + * 自定义行样式 + */ + rowClassNameExpr?: string; + + /** + * 是否固定内容行高度 + */ + lineHeight?: string; + + /** + * 是否展示边框 + */ + bordered?: boolean; + + /** + * 是否展示表头 + */ + showHeader?: boolean; + + /** + * 自定义表格样式 + */ + className?: string; + + /** + * 指定表尾 + */ + footer?: string | SchemaObject | Array; } export interface TableV2Props extends RendererProps { @@ -158,51 +320,273 @@ export interface TableV2Props extends RendererProps { }) export class TableRenderer extends React.Component { renderedToolbars: Array = []; + control: any; constructor(props: TableV2Props) { super(props); this.handleColumnToggle = this.handleColumnToggle.bind(this); + this.getPopOverContainer = this.getPopOverContainer.bind(this); + this.handleAction = this.handleAction.bind(this); + this.handleQuickChange = this.handleQuickChange.bind(this); + this.handleSave = this.handleSave.bind(this); + this.controlRef = this.controlRef.bind(this); - const {store, columnsToggable, columns} = props; + const {store, columnsTogglable, columns} = props; - store.update({columnsToggable, columns}); + store.update({columnsTogglable, columns}); + TableRenderer.syncRows(store, props, undefined) && this.syncSelected(); } - renderSchema(schema: any, props: any) { - const {render} = this.props; - // Table Header、Footer SchemaObject转化成ReactNode - if (schema && isObject(schema)) { - return render('field', schema, props); + controlRef(control: any) { + // 因为 control 有可能被 n 层 hoc 包裹。 + while (control && control.getWrappedInstance) { + control = control.getWrappedInstance(); } + + this.control = control; + } + + syncSelected() { + const {store, rowSelection} = this.props; + + rowSelection && rowSelection.onSelect && + rowSelection.onSelectonSelect( + store.selectedRowKeys.map(item => item), + store.selectedRows.map(item => item.data) + ); + } + + static syncRows( + store: ITableStore, + props: TableV2Props, + prevProps?: TableV2Props + ) { + const source = props.source; + const value = props.value || props.items; + let rows: Array = []; + let updateRows = false; + + if ( + Array.isArray(value) && + (!prevProps || (prevProps.value || prevProps.items) !== value) + ) { + updateRows = true; + rows = value; + } else if (typeof source === 'string') { + const resolved = resolveVariableAndFilter(source, props.data, '| raw'); + const prev = prevProps + ? resolveVariableAndFilter(source, prevProps.data, '| raw') + : null; + + if (prev && prev === resolved) { + updateRows = false; + } else if (Array.isArray(resolved)) { + updateRows = true; + rows = resolved; + } + } + updateRows && store.initRows(rows, props.getEntryId, props.reUseRow, props.childrenColumnName); + + let selectedRowKeys: Array = []; + // selectedRowKeysExpr比selectedRowKeys优先级高 + if (props.rowSelection && props.rowSelection.selectedRowKeysExpr) { + rows.forEach((row: any, index: number) => { + const flag = filter(props.rowSelection.selectedRowKeysExpr, {record: row, rowIndex: index}); + if (flag === 'true') { + selectedRowKeys.push(row[props?.rowSelection?.keyField || 'key']); + } + }); + } else if (props.rowSelection && props.rowSelection.selectedRowKeys) { + selectedRowKeys = [...props.rowSelection.selectedRowKeys]; + } + + if (updateRows && selectedRowKeys.length > 0) { + store.updateSelected( + selectedRowKeys, + props.rowSelection.keyField + ); + } + + let expandedRowKeys: Array = []; + if (props.expandable && props.expandable.expandedRowKeysExpr) { + rows.forEach((row: any, index: number) => { + const flag = filter(props.expandable.expandedRowKeysExpr, {record: row, rowIndex: index}); + if (flag === 'true') { + expandedRowKeys.push(row[props?.expandable?.keyField || 'key']); + } + }); + } else if (props.expandable && props.expandable.expandedRowKeys) { + expandedRowKeys = [...props.expandable.expandedRowKeys]; + } + + if (updateRows && expandedRowKeys.length > 0) { + store.updateExpanded(expandedRowKeys, props.expandable.keyField); + } + + return updateRows; + } + + componentDidUpdate(prevProps: TableV2Props) { + const props = this.props; + const store = props.store; + + if ( + anyChanged( + [ + 'columnsTogglable' + ], + prevProps, + props + ) + ) { + store.update({ + columnsTogglable: props.columnsTogglable + }); + } + + if ( + anyChanged(['source', 'value', 'items'], prevProps, props) || + (!props.value && + !props.items && + (props.data !== prevProps.data || + (typeof props.source === 'string' && isPureVariable(props.source)))) + ) { + TableRenderer.syncRows(store, props, prevProps) && this.syncSelected(); + } + + if (!isEqual(prevProps.columns, props.columns)) { + store.update({ + columns: props.columns + }); + } + } + + getPopOverContainer() { + return findDOMNode(this); + } + + renderCellSchema(schema: any, props: any) { + const {render} = this.props; + + // Table Cell SchemaObject转化成ReactNode + if (schema && isObject(schema)) { + // 在TableCell里会根据width设置div的width + // 原来的table td/th是最外层标签 设置width没问题 + // v2的拆开了 就不需要再设置div的width了 + // 否则加上padding 就超出单元格的区域了 + // children属性在schema里是一个关键字 在渲染器schema中 自定义的children没有用 去掉 + const {width, children, ...rest} = schema; + return render('cell-field', { + ...rest, + type: 'cell-field', + column: rest, + data: props.data, + name: schema.key + }, props); + } + return schema; } - getColumns(columns: Array) { - const cols: Array = []; + renderSchema(key: string, schema: any, props?: any) { + const {render} = this.props; + + // Header、Footer等SchemaObject转化成ReactNode + if (schema && isObject(schema)) { + return render(key || 'field', {...schema, data: props.data}, props); + } else if (Array.isArray(schema)) { + const renderers: Array = []; + schema.forEach((s, i) => renderers.push(render(key || 'field', { + ...s, + data: props.data + }, {...props, key: i}))); + return renderers; + } + + return schema; + } + // editor传来的处理过的column 还可能包含其他字段 + buildColumns(columns: Array) { + const { + env, + render, + store, + popOverContainer, + canAccessSuperData, + showBadge, + itemBadge, + classnames: cx + } = this.props; + + const cols: Array = []; const rowSpans: Array = []; const colSpans: Array = []; - columns.forEach((column, col) => { - const clone = {...column} as ColumnProps; + + Array.isArray(columns) && columns.forEach((column, col) => { + const clone = {...column} as any; + + let titleSchema: any = null; + const titleProps = { + popOverContainer: popOverContainer || this.getPopOverContainer, + value: column.title + }; if (isObject(column.title)) { - const title = cloneDeep(column.title); - Object.assign(clone, { - title: () => this.renderSchema(title, {}) - }); + titleSchema = cloneDeep(column.title); } else if (typeof column.title === 'string') { - Object.assign(clone, { - title: () => this.renderSchema({type: 'plain'}, {value: column.title}) - }); + titleSchema = {type: 'plain'}; } + const titleRender = (children: any) => { + const content = this.renderCellSchema(titleSchema, titleProps); + + let remark = null; + if (column.remark) { + remark = render('remark', { + type: 'remark', + tooltip: column.remark, + container: + env && env.getModalContainer + ? env.getModalContainer + : undefined + }); + } + + return
    + {content}{remark}{children} +
    ; + }; + + Object.assign(clone, { + title: titleRender + }); + + // 设置了type值 就完全按渲染器处理了 if (column.type) { Object.assign(clone, { render: (text: string, record: any, rowIndex: number, colIndex: number) => { const props: RenderProps = {}; + const item = store.getRowByIndex(rowIndex); const obj = { - children: this.renderSchema(column, { - data: record, - value: record[column.key] + children: this.renderCellSchema(column, { + data: item.locals, + value: column.key + ? resolveVariable( + column.key, + canAccessSuperData ? item.locals : item.data + ) + : column.key, + popOverContainer: popOverContainer || this.getPopOverContainer, + onQuickChange: ( + values: object, + saveImmediately?: boolean, + savePristine?: boolean, + resetOnFailed?: boolean) => { + this.handleQuickChange(item, values, saveImmediately, savePristine, resetOnFailed) + }, + row: item, + showBadge, + itemBadge }), props }; @@ -243,16 +627,31 @@ export class TableRenderer extends React.Component { }); } + // 设置了列搜索 + if (column.searchable) { + clone.filterDropdown = (); + } + if (column.children) { - clone.children = this.getColumns(column.children); + clone.children = this.buildColumns(column.children); } cols.push(clone); }); + return cols; } - getSummary(summary: Array) { + buildSummary(key: string, summary: Array) { const result: Array = []; if (Array.isArray(summary)) { summary.forEach((s, index) => { @@ -260,8 +659,8 @@ export class TableRenderer extends React.Component { result.push({ colSpan: s.colSpan, fixed: s.fixed, - render: () => this.renderSchema(s, { - data: this.props.data + render: (dataSouce: Array) => this.renderSchema(key, s, { + data: dataSouce }) }); } else if (Array.isArray(s)) { @@ -272,18 +671,155 @@ export class TableRenderer extends React.Component { result[index].push({ colSpan: d.colSpan, fixed: d.fixed, - render: () => this.renderSchema(s, { - data: this.props.data + render: (dataSouce: Array) => this.renderSchema(key, d, { + data: dataSouce }) }); }); } - }) + }); } return result.length ? result : null; } + reloadTarget(target: string, data: any) { + const scoped = this.context as IScopedContext; + scoped.reload(target, data); + } + + handleSave( + rows: Array | object, + diff: Array | object, + indexes: Array, + unModifiedItems?: Array, + rowsOrigin?: Array | object, + resetOnFailed?: boolean + ) { + const { + store, + quickSaveApi, + quickSaveItemApi, + primaryField, + env, + messages, + reload + } = this.props; + + if (Array.isArray(rows)) { + if (!isEffectiveApi(quickSaveApi)) { + env && env.alert('TableV2 quickSaveApi is required'); + return; + } + + const data: any = createObject(store.data, { + rows, + rowsDiff: diff, + indexes: indexes, + rowsOrigin + }); + + if (rows.length && rows[0].hasOwnProperty(primaryField || 'id')) { + data.ids = rows + .map(item => (item as any)[primaryField || 'id']) + .join(','); + } + + if (unModifiedItems) { + data.unModifiedItems = unModifiedItems; + } + + store + .saveRemote(quickSaveApi, data, { + successMessage: messages && messages.saveFailed, + errorMessage: messages && messages.saveSuccess + }) + .then(() => { + reload && this.reloadTarget(reload, data); + }) + .catch(() => {}); + } else { + if (!isEffectiveApi(quickSaveItemApi)) { + env && env.alert('TableV2 quickSaveItemApi is required!'); + return; + } + + const data = createObject(store.data, { + item: rows, + modified: diff, + origin: rowsOrigin + }); + + const sendData = createObject(data, rows); + store + .saveRemote(quickSaveItemApi, sendData) + .then(() => { + reload && this.reloadTarget(reload, data); + }) + .catch(() => { + resetOnFailed && this.control.reset(); + }); + } + } + + handleQuickChange( + item: IRow, + values: object, + saveImmediately?: boolean | any, + savePristine?: boolean, + resetOnFailed?: boolean + ) { + + if (!isAlive(item)) { + return; + } + + const { + onSave, + onPristineChange, + primaryField, + quickSaveItemApi + } = this.props; + + item.change(values, savePristine); + + // 值发生变化了,需要通过 onSelect 通知到外面,否则会出现数据不同步的问题 + item.modified && this.syncSelected(); + + if (savePristine) { + onPristineChange?.(item.data, item.path); + return; + } + + if (saveImmediately && saveImmediately.api) { + this.props.onAction( + null, + { + actionType: 'ajax', + api: saveImmediately.api + }, + values + ); + return; + } + + onSave ? onSave( + item.data, + difference(item.data, item.pristine, ['id', primaryField]), + item.path, + undefined, + item.pristine, + resetOnFailed + ) : this.handleSave( + quickSaveItemApi ? item.data : [item.data], + difference(item.data, item.pristine, ['id', primaryField]), + [item.path], + undefined, + item.pristine, + resetOnFailed + ); + } + handleColumnToggle(columns: Array) { const {store} = this.props; @@ -320,20 +856,22 @@ export class TableRenderer extends React.Component { classPrefix={ns} key="columns-toggable" size={config?.size || 'sm'} + icon={config?.icon} label={ - config?.label || + config?.label || '' } draggable={config?.draggable} columns={store.columnsData} onColumnToggle={this.handleColumnToggle} > - {store.toggableColumns.map(column => ( + {store.toggableColumns.map((column, index) => (
  • @@ -345,21 +883,15 @@ export class TableRenderer extends React.Component { ); } - renderToolbar(toolbar: SchemaNode) { - const type = (toolbar as Schema).type || (toolbar as string); + handleAction(e: React.UIEvent, action: Action, ctx: object) { + const {onAction} = this.props; - if (type === 'columns-toggler') { - this.renderedToolbars.push(type); - return this.renderColumnsToggler(toolbar as any); - } - - return void 0; + // todo + onAction(e, action, ctx); } - // handleAction() {} - renderActions(region: string) { - let {actions, render, store, classnames: cx, data} = this.props; + let {actions, render, store, classnames: cx, data, columnsToggler} = this.props; actions = Array.isArray(actions) ? actions.concat() : []; @@ -370,7 +902,7 @@ export class TableRenderer extends React.Component { ) { actions.push({ type: 'button', - children: this.renderColumnsToggler() + children: this.renderColumnsToggler(columnsToggler) }); } @@ -384,10 +916,10 @@ export class TableRenderer extends React.Component { ...(action as any) }, { - // onAction: this.handleAction, - key - // btnDisabled: store.dragging, - // data: store.getData(data) + onAction: this.handleAction, + key, + btnDisabled: store.dragging, + data: store.getData(data) } ) )} @@ -400,71 +932,142 @@ export class TableRenderer extends React.Component { render, title, footer, - source, - columns, rowSelection, + columns, expandable, + expandableBody, footSummary, headSummary, + loading, classnames: cx, + placeholder, + rowClassNameExpr, store, ...rest } = this.props; - let sourceValue = this.props.data.items; - - if (typeof source === 'string') { - sourceValue = resolveVariableAndFilter(source, this.props.data, '| raw'); - } - + let expandableConfig: any = null; if (expandable) { - if (expandable.expandableOn) { - const expandableOn = cloneDeep(expandable.expandableOn); - expandable.rowExpandable = (record: any) => { - return evalExpression(expandableOn, record); - }; - delete expandable.expandableOn; + const {expandedRowKeys, ...rest} = expandable; + + expandableConfig = { + expandedRowKeys: store.currentExpandedKeys, + ...rest } - if (expandable.type) { - expandable.expandedRowRender = (record: any, rowIndex: number) => { - return this.renderSchema(expandable, {data: record}); - }; + if (expandable.expandableOn) { + expandableConfig.rowExpandable = (record: any, rowIndex: number) => + evalExpression(expandable.expandableOn, {record, rowIndex}); + delete expandableConfig.expandableOn; + } + + if (expandableBody && expandableBody.length > 0) { + expandableConfig.expandedRowRender = (record: any, rowIndex: number) => + this.renderSchema('expandableBody', expandableBody, {data: record}); } if (expandable.expandedRowClassNameExpr) { - const expandedRowClassNameExpr = cloneDeep(expandable.expandedRowClassNameExpr); - expandable.expandedRowClassName = (record: any, rowIndex: number) => { - return filter(expandedRowClassNameExpr, {record, rowIndex}); - }; - delete expandable.expandedRowClassNameExpr; + expandableConfig.expandedRowClassName = (record: any, rowIndex: number) => + filter(expandable.expandedRowClassNameExpr, {record, rowIndex}); + delete expandableConfig.expandedRowClassNameExpr; } } + let rowSelectionConfig: any = null; if (rowSelection) { - if (rowSelection.disableOn) { - const disableOn = cloneDeep(rowSelection.disableOn); + const {selectedRowKeys, selections, ...rest} = rowSelection; + rowSelectionConfig = { + selectedRowKeys: store.currentSelectedRowKeys, + ...rest + }; - rowSelection.getCheckboxProps = (record: any, rowIndex: number) => { - return { - disabled: evalExpression(disableOn, {record, rowIndex}) - }; - }; + if (rowSelection.disableOn) { + const disableOn = rowSelection.disableOn; - delete rowSelection.disableOn; - } + rowSelectionConfig.getCheckboxProps = (record: any, rowIndex: number) => ({ + disabled: evalExpression(disableOn, {record, rowIndex}) + }); + + delete rowSelectionConfig.disableOn; + } + + if (selections && Array.isArray(selections)) { + rowSelectionConfig.selections = []; + + selections.forEach((item: RowSelectionOptionsSchema) => { + rowSelectionConfig.selections.push({ + key: item.key, + text: item.text, + onSelect: (changableRowKeys: Array) => { + let newSelectedRowKeys = []; + newSelectedRowKeys = changableRowKeys.filter((key, index) => { + if (item.key === 'all') { + return true; + } + if (item.key === 'none') { + return false; + } + if (item.key === 'invert') { + return !store.currentSelectedRowKeys.includes(key); + } + // 奇数行 + if (item.key === 'odd') { + if (index % 2 !== 0) { + return false; + } + return true; + } + // 偶数行 + if (item.key === 'even') { + if (index % 2 !== 0) { + return true; + } + return false; + } + return true; + }); + store.updateSelected(newSelectedRowKeys, rowSelection.keyField); + } + }) + }); + } + + // 因为要通过原子组件Table同步store里的selectedRows + // 因此onSelect在这里处理一下 + rowSelectionConfig.onSelect = ( + record: any, + value: boolean, + selectedRows: Array, + selectedRowKeys: Array + ) => { + store.updateSelected(selectedRowKeys, rowSelection.keyField); + + rowSelection.onSelect + && rowSelection.onSelect(record, value, selectedRows, selectedRowKeys); + }; + } + + let rowClassName = undefined; + // 设置了行样式 + if (rowClassNameExpr) { + rowClassName = (record: any, rowIndex: number) => { + return filter(rowClassNameExpr, {record, rowIndex}); + }; } return
  • + {...rest} + title={this.renderSchema('title', title, {data: this.props.data})} + footer={this.renderSchema('footer', footer, {data: this.props.data})} + columns={this.buildColumns(store.filteredColumns)} + dataSource={store.dataSource} + rowSelection={rowSelectionConfig} + rowClassName={rowClassName} + expandable={expandableConfig} + footSummary={this.buildSummary('footSummary', footSummary)} + headSummary={this.buildSummary('headSummary', headSummary)} + loading={this.renderSchema('loading', loading)} + placeholder={this.renderSchema('placeholder', placeholder)}>
    ; } @@ -475,7 +1078,7 @@ export class TableRenderer extends React.Component { this.renderedToolbars = []; // 用来记录哪些 toolbar 已经渲染了 - return
    + return
    {this.renderActions('header')} {this.renderTable()}
    ; diff --git a/src/renderers/Table/HeadCellSearchDropdown.tsx b/src/renderers/Table/HeadCellSearchDropdown.tsx index 6b9b9f939..204fbdd92 100644 --- a/src/renderers/Table/HeadCellSearchDropdown.tsx +++ b/src/renderers/Table/HeadCellSearchDropdown.tsx @@ -37,7 +37,6 @@ export class HeadCellSearchDropDown extends React.Component< this.open = this.open.bind(this); this.close = this.close.bind(this); - this.close = this.close.bind(this); this.handleSubmit = this.handleSubmit.bind(this); this.handleAction = this.handleAction.bind(this); } diff --git a/src/renderers/Table/TableBody.tsx b/src/renderers/Table/TableBody.tsx index f9cd28d19..c02b3ea86 100644 --- a/src/renderers/Table/TableBody.tsx +++ b/src/renderers/Table/TableBody.tsx @@ -156,16 +156,14 @@ export class TableBody extends React.Component { classnames: cx, rows, prefixRowClassName, - affixRowClassName, - footable + affixRowClassName } = this.props; if (!(Array.isArray(items) && items.length)) { return null; } - // 开启了footable,不需要考虑设置了breakpoint的列了 - const filterColumns = columns.filter(item => item.toggable && !(footable && item.breakpoint)); + const filterColumns = columns.filter(item => item.toggable); const result: any[] = []; for (let index = 0; index < filterColumns.length; index++) { @@ -184,16 +182,14 @@ export class TableBody extends React.Component { } // 缺少的单元格补齐 - // 考虑是否设置了 - // 开启了footable,不需要考虑设置了breakpoint的列了 const appendLen = - (footable ? columns.filter(item => !item.breakpoint).length : columns.length) - result.reduce((p, c) => p + (c.colSpan || 1), 0); + 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/table-v2.ts b/src/store/table-v2.ts index 9eb864617..1b595f441 100644 --- a/src/store/table-v2.ts +++ b/src/store/table-v2.ts @@ -3,11 +3,35 @@ import { getParent, Instance, SnapshotIn, - isAlive + isAlive, + IAnyModelType, + flow, + getEnv } from 'mobx-state-tree'; +import find from 'lodash/find'; +import isEqual from 'lodash/isEqual'; -import {isVisible, hasVisibleExpression} from '../utils/helper'; -import {iRendererStore} from './iRenderer'; +import { + isVisible, + hasVisibleExpression, + isObjectShallowModified, + qsstringify, + guid, + eachTree, + createObject, + flattenTree, + isObject, + immutableExtends, + isEmpty, + extendObject +} from '../utils/helper'; +import {normalizeApiResponseData} from '../utils/api'; +import {Api, Payload, fetchOptions, ApiObject} from '../types'; +import {ServiceStore} from './service'; + +class ServerError extends Error { + type = 'ServerError'; +} export const Column = types .model('Column', { @@ -18,7 +42,11 @@ export const Column = types pristine: types.optional(types.frozen(), undefined), toggable: true, index: 0, - type: '' + type: '', + children: types.optional( + types.array(types.late((): IAnyModelType => Column)), + [] + ) }) .actions(self => ({ toggleToggle() { @@ -41,29 +69,133 @@ export type SColumn = SnapshotIn; export const Row = types .model('Row', { - data: types.frozen({} as any) - }); + storeType: 'Row', + id: types.identifier, + key: types.string, + pristine: types.frozen({} as any), // 原始数据 + data: types.frozen({} as any), + index: types.number, + newIndex: types.number, + depth: types.number, // 当前children位于第几层,便于使用getParent获取最顶层TableStore + children: types.optional( + types.array(types.late((): IAnyModelType => Row)), + [] + ), + path: '' // 行数据的位置 + }) + .views(self => ({ + get checked(): boolean { + return (getParent(self, self.depth * 2) as ITableStore).isSelected( + self as IRow + ); + }, + + get modified() { + if (!self.data) { + return false; + } + + return Object.keys(self.data).some( + key => !isEqual(self.data[key], self.pristine[key]) + ); + }, + + get locals(): any { + let children: Array | null = null; + if (self.children.length) { + children = self.children.map(item => item.locals); + } + + const parent = getParent(self, 2) as ITableStore; + return createObject( + extendObject((getParent(self, self.depth * 2) as ITableStore).data, { + index: self.index, + // todo 以后再支持多层,目前先一层 + parent: parent.storeType === Row.name ? parent.data : undefined + }), + children + ? { + ...self.data, + children + } + : self.data + ); + } + })) + .actions(self => ({ + replaceWith(data: any) { + Object.keys(data).forEach(key => { + if (key !== 'id') { + (self as any)[key] = data[key]; + } + }); + + if (Array.isArray(data.children)) { + const arr = data.children; + const pool = arr.concat(); + + // 把多的删了先 + if (self.children.length > arr.length) { + self.children.splice(arr.length, self.children.length - arr.length); + } + + let index = 0; + const len = self.children.length; + while (pool.length) { + // 因为父级id未更新,所以需要将子级的parentId正确指向父级id + const item = { + ...pool.shift(), + parentId: self.id + }!; + + if (index < len) { + self.children[index].replaceWith(item); + } else { + const row = Row.create(item); + self.children.push(row); + } + index++; + } + } + }, + change(values: object, savePristine?: boolean) { + self.data = immutableExtends(self.data, values); + savePristine && (self.pristine = self.data); + } + })); export type IRow = Instance; export type SRow = SnapshotIn; -export const TableStoreV2 = iRendererStore +export const TableStoreV2 = ServiceStore .named('TableStoreV2') .props({ columns: types.array(Column), rows: types.array(Row), - columnsToggable: types.optional( + selectedRowKeys: types.array(types.frozen()), + selectedRows: types.array(types.reference(Row)), + expandedRowKeys: types.array(types.frozen()), + columnsTogglable: types.optional( types.union(types.boolean, types.literal('auto')), 'auto' - ) + ), + orderBy: '', + order: types.optional( + types.union(types.literal('asc'), types.literal('desc')), + 'asc' + ), + query: types.optional(types.frozen(), {}), + pageNo: 1, + pageSize: 10, + dragging: false }) .views(self => { function getToggable() { - if (self.columnsToggable === 'auto') { + if (self.columnsTogglable === 'auto') { return self.columns.filter.length > 10; } - return self.columnsToggable; + return self.columnsTogglable; } function hasColumnHidden() { @@ -80,16 +212,43 @@ export const TableStoreV2 = iRendererStore return getToggableColumns().filter(item => item.toggled); } + function getAllFilteredColumns(columns?: Array): Array { + if (columns) { + return columns.filter( + item => + item && + isVisible( + item.pristine, + hasVisibleExpression(item.pristine) ? self.data : {} + ) && + (item.toggled || !item.toggable) + ).map(item => ({ + ...item.pristine, + type: item.type, + children: item.children ? getAllFilteredColumns(item.children) : undefined + })); + } + return []; + } + 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 getAllFilteredColumns(self.columns); + } + + function getUnSelectedRows() { + return flattenTree(self.rows).filter((item: IRow) => !item.checked); + } + + function getData(superData: any): any { + return createObject(superData, { + items: self.rows.map(item => item.data), + selectedItems: self.selectedRows.map(item => item.data), + unSelectedItems: getUnSelectedRows().map(item => item.data) + }); + } + + function isSelected(row: IRow): boolean { + return !!~self.selectedRows.indexOf(row); } return { @@ -113,32 +272,63 @@ export const TableStoreV2 = iRendererStore return getActiveToggableColumns(); }, + get dataSource() { + return self.rows.map(item => item.data); + }, + + get currentSelectedRowKeys() { + return self.selectedRowKeys.map(item => item); + }, + + get currentExpandedKeys() { + return self.expandedRowKeys.map(item => item); + }, + // 是否隐藏了某列 hasColumnHidden() { return hasColumnHidden(); - } + }, + + getData, + + isSelected } }) .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 + function updateColumns(columns: Array) { + if (columns && Array.isArray(columns)) { + let cols: Array = columns .filter(column => column) .concat(); - columns = columns.map((item, index) => ({ - ...item, + cols = cols.map((item, index) => ({ + ...item, index, type: item.type || 'plain', pristine: item, toggled: item.toggled !== false, - breakpoint: item.breakpoint + breakpoint: item.breakpoint, + children: item.children ? updateColumns(item.children) : [] })); - self.columns.replace(columns as any); + return cols; + } + return; + } + + function update(config: Partial) { + config.columnsTogglable !== void 0 && + (self.columnsTogglable = config.columnsTogglable); + + if (typeof config.orderBy === 'string') { + setOrderByInfo( + config.orderBy, + config.order === 'desc' ? 'desc' : 'asc' + ); + } + + if (config.columns && Array.isArray(config.columns)) { + self.columns.replace(updateColumns(config.columns) as any); } } @@ -153,9 +343,275 @@ export const TableStoreV2 = iRendererStore ); } + function setOrderByInfo(key: string, direction: 'asc' | 'desc') { + self.orderBy = key; + self.order = direction; + } + + function updateQuery( + values: object, + updater?: Function, + pageNoField: string = 'pageNo', + pageSizeField: string = 'pageSize', + replace: boolean = false + ) { + const originQuery = self.query; + self.query = replace + ? { + ...values + } + : { + ...self.query, + ...values + }; + + if (self.query[pageNoField || 'pageNo']) { + self.pageNo = parseInt(self.query[pageNoField || 'pageNo'], 10); + } + + if (self.query[pageSizeField || 'pageSize']) { + self.pageSize = parseInt(self.query[pageSizeField || 'pageSize'], 10); + } + + updater && + isObjectShallowModified(originQuery, self.query, false) && + setTimeout(updater.bind(null, `?${qsstringify(self.query)}`), 4); + } + + function updateSelectedRows(rows: Array, selectedKeys: Array, keyField?: string) { + eachTree(rows, item => { + if (~selectedKeys.indexOf(item.pristine[keyField || 'key'])) { + self.selectedRows.push(item.id); + self.selectedRowKeys.push(item.pristine[keyField || 'key']); + } else if ( + find( + selectedKeys, + a => + a && + a == item.pristine[keyField || 'key'] + ) + ) { + self.selectedRows.push(item.id); + self.selectedRowKeys.push(item.pristine[keyField || 'key']); + } else if (item.children) { + updateSelectedRows(item.children, selectedKeys, keyField); + } + }); + } + + function updateSelected(selectedKeys: Array, keyField?: string) { + self.selectedRows.clear(); + self.selectedRowKeys.clear(); + + updateSelectedRows(self.rows, selectedKeys, keyField); + } + + function updateExpanded(expandedRowKeys: Array, keyField?: string) { + self.expandedRowKeys.clear(); + + eachTree(self.rows, item => { + if (~expandedRowKeys.indexOf(item.pristine[keyField || 'key'])) { + self.expandedRowKeys.push(item.pristine[keyField || 'key']); + } else if ( + find( + expandedRowKeys, + a => + a && + a == item.pristine[keyField || 'key'] + ) + ) { + self.expandedRowKeys.push(item.pristine[keyField || 'key']); + } + }); + } + + // 尽可能的复用 row + function replaceRow(arr: Array, reUseRow?: boolean) { + if (reUseRow === false) { + self.rows.replace(arr.map(item => Row.create(item))); + return; + } + + const pool = arr.concat(); + + // 把多的删了先 + if (self.rows.length > arr.length) { + self.rows.splice(arr.length, self.rows.length - arr.length); + } + + let index = 0; + const len = self.rows.length; + while (pool.length) { + const item = pool.shift()!; + + if (index < len) { + self.rows[index].replaceWith(item); + } else { + const row = Row.create(item); + self.rows.push(row); + } + index++; + } + } + + function initChildren( + children: Array, + depth: number, + pindex: number, + parentId: string, + path: string = '', + keyField?: string + ): any { + const key = keyField || 'children'; + + depth += 1; + return children.map((item, index) => { + item = isObject(item) + ? item + : { + item + }; + const id = guid(); + + return { + id: id, + parentId, + key: String(`${pindex}-${depth}-${index}`), + path: `${path}${index}`, + depth: depth, + index: index, + newIndex: index, + pristine: item, + data: item, + rowSpans: {}, + children: + item && Array.isArray(item[key]) + ? initChildren( + item[key], + depth, + index, + id, + `${path}${index}.`, + + ) + : [] + }; + }); + } + + function initRows( + rows: Array, + getEntryId?: (entry: any, index: number) => string, + reUseRow?: boolean, + keyField?: string + ) { + self.selectedRows.clear(); + + const key = keyField || 'children'; + + let arr: Array = rows.map((item, index) => { + let id = getEntryId ? getEntryId(item, index) : guid(); + + return { + id: id, + key: String(`${index}-1-${index}`), + index: index, + newIndex: index, + pristine: item, + path: `${index}`, + data: item, + depth: 1, // 最大父节点默认为第一层,逐层叠加 + children: + item && Array.isArray(item[key]) + ? initChildren(item[key], 1, index, id, `${index}.`, key) + : [] + }; + }); + + replaceRow(arr, reUseRow); + } + + const saveRemote: ( + api: Api, + data?: object, + options?: fetchOptions + ) => Promise = flow(function* saveRemote( + api: Api, + data: object, + options: fetchOptions = {} + ) { + try { + options = { + method: 'post', // 默认走 post + ...options + }; + + self.markSaving(true); + const json: Payload = yield getEnv(self).fetcher(api, data, options); + self.markSaving(false); + + if (!isEmpty(json.data) || json.ok) { + self.updateData( + normalizeApiResponseData(json.data), + { + __saved: Date.now() + }, + !!api && (api as ApiObject).replaceData + ); + self.updatedAt = Date.now(); + } + + if (!json.ok) { + self.updateMessage( + json.msg ?? options.errorMessage ?? self.__('saveFailed'), + true + ); + getEnv(self).notify( + 'error', + self.msg, + json.msgTimeout !== undefined + ? { + closeButton: true, + timeout: json.msgTimeout + } + : undefined + ); + throw new ServerError(self.msg); + } else { + self.updateMessage(json.msg ?? options.successMessage); + self.msg && + getEnv(self).notify( + 'success', + self.msg, + json.msgTimeout !== undefined + ? { + closeButton: true, + timeout: json.msgTimeout + } + : undefined + ); + } + return json.data; + } catch (e) { + self.markSaving(false); + + if (!isAlive(self) || self.disposed) { + return; + } + + e.type !== 'ServerError' && getEnv(self).notify('error', e.message); + throw e; + } + }); + return { update, persistSaveToggledColumns, + setOrderByInfo, + updateQuery, + initRows, + updateSelected, + updateExpanded, // events afterCreate() { @@ -177,6 +633,11 @@ export const TableStoreV2 = iRendererStore ); } }, 200); + }, + saveRemote, + + getRowByIndex(rowIndex: number) { + return self.rows[rowIndex]; } }; }); diff --git a/src/store/table.ts b/src/store/table.ts index 2584e4690..301331c90 100644 --- a/src/store/table.ts +++ b/src/store/table.ts @@ -639,8 +639,7 @@ export const TableStore = iRendererStore }, get disabledHeadCheckbox() { - // 设置为multiple 默认没选择会报错 - const selectedLength = self.data?.selectedItems?.length; + const selectedLength = self.data?.selectedItems.length; const maxLength = self.maxKeepItemSelectionLength; if (!self.data || !self.keepItemSelectionOnPageChange || !maxLength) {