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) {