feat: Table原子组件

This commit is contained in:
wanglinfang 2021-12-24 11:05:02 +08:00
parent 0080d01487
commit 422376dec7
17 changed files with 3349 additions and 9 deletions

901
docs/zh-CN/components/table-v2.md Executable file
View File

@ -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": "<div class=\"test\">测试测试</div>",
"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<Column>` | | 用来设置列信息 |
| 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 | | | 提示信息 |

View File

@ -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',

View File

@ -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;

View File

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

View File

@ -200,6 +200,7 @@ export type SchemaType =
| 'switch'
| 'table'
| 'static-table' // 这个几个跟表单项同名再form下面用必须带前缀 static-
| 'table-v2'
| 'tabs'
| 'html'
| 'tpl'

View File

@ -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
};

View File

@ -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<Props> {
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 (
<th
key={key || null}
rowSpan={rowSpan && rowSpan > 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}</th>
);
}
return (
<td
key={key || null}
rowSpan={rowSpan && rowSpan > 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}</td>
);
}
}
export default themeable(localeable(BodyCell));

View File

@ -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<string>;
filterMultiple?: boolean;
}
export interface OptionProps {
text: string;
value: string;
selected?: boolean;
children?: Array<OptionProps>;
}
export interface State {
options: Array<OptionProps>;
isOpened: boolean;
filteredValue: Array<string>;
}
export class HeadCellFilter extends React.Component<Props, State> {
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<any>) {
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 (
<span
className={cx(
`${ns}TableCell-filterBtn`,
column.filtered || options && options.some((item: any) => item.selected) ? 'is-active' : ''
)}
>
<span onClick={this.openLayer}>
<Icon icon="column-filter" className="icon" />
</span>
{
isOpened ? (
<Overlay
container={popOverContainer || (() => findDOMNode(this))}
placement="left-bottom-left-top right-bottom-right-top"
target={
popOverContainer ? () => findDOMNode(this) : null
}
show
>
<PopOver
classPrefix={ns}
onHide={this.closeLayer}
className={cx(
`${ns}TableCell-filterPopOver`
)}
overlay
>
{options && options.length > 0 ? (
<ul className={cx('DropDown-menu')}>
{!column.filterMultiple
? options.map((option: any, index) => (
<li
key={index}
className={cx({
'is-active': option.selected
})}
onClick={this.handleClick.bind(this, option.value)}
>
{option.text}
</li>
))
: options.map((option: any, index) => (
<li key={index}>
<CheckBox
classPrefix={ns}
onChange={this.handleCheck.bind(this, option.value)}
checked={option.selected}
>
{option.text}
</CheckBox>
</li>
))}
{column.filterMultiple ? (
<li
key="DropDown-multiple-menu"
className={cx('DropDown-multiple-menu')}
>
<Button
size={'xs'}
level={'primary'}
onClick={this.handleConfirmClick.bind(this)}
></Button>
<Button
size={'xs'}
onClick={this.handleCancelClick.bind(this)}
></Button>
</li>
) : null}
</ul>
) : null}
</PopOver>
</Overlay>)
: null
}
</span>
);
}
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));

View File

@ -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<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
order: '',
orderBy: ''
};
}
render() {
const {
column,
onSort,
classnames: cx
} = this.props;
return (
<span
className={cx('TableCell-sortBtn')}
onClick={() => {
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);
}}
>
<i
className={cx(
'TableCell-sortBtn--down',
this.state.orderBy === column.key && this.state.order === 'descend'
? 'is-active'
: ''
)}
>
<Icon icon="sort-desc" className="icon" />
</i>
<i
className={cx(
'TableCell-sortBtn--up',
this.state.orderBy === column.key && this.state.order === 'ascend'
? 'is-active'
: ''
)}
>
<Icon icon="sort-asc" className="icon" />
</i>
<i
className={cx(
'TableCell-sortBtn--default',
this.state.orderBy === column.key ? '' : 'is-active'
)}
>
<Icon icon="sort-default" className="icon" />
</i>
</span>
);
}
}
export default themeable(localeable(HeadCellSort));

File diff suppressed because it is too large Load Diff

View File

@ -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';

View File

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

View File

@ -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<ColumnSchema>
}
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<ColumnSchema>;
/**
*
*/
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<TableV2Props, object> {
renderedToolbars: Array<string> = [];
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<ColumnSchema>) {
const cols: Array<ColumnProps> = [];
const rowSpans: Array<CellSpan> = [];
const colSpans: Array<CellSpan> = [];
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<any>) {
const result: Array<any> = [];
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<IColumn>) {
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 (
<ColumnToggler
{...rest}
{...(isObject(config) ? config : {})}
tooltip={config?.tooltip || __('Table.columnsVisibility')}
tooltipContainer={
env && env.getModalContainer ? env.getModalContainer : undefined
}
align={config?.align ?? 'left'}
isActived={store.hasColumnHidden()}
classnames={cx}
classPrefix={ns}
key="columns-toggable"
size={config?.size || 'sm'}
label={
config?.label || <Icon icon="columns" className="icon m-r-none" />
}
draggable={config?.draggable}
columns={store.columnsData}
onColumnToggle={this.handleColumnToggle}
>
{store.toggableColumns.map(column => (
<li
className={cx('ColumnToggler-menuItem')}
key={column.index}
onClick={column.toggleToggle}
>
<Checkbox
size="sm"
classPrefix={ns}
checked={column.toggled}>
{column.title ? render('tpl', column.title) : null}
</Checkbox>
</li>
))}
</ColumnToggler>
);
}
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 ? (
<div className={cx('Table-toolbar')}>
{actions.map((action, key) =>
render(
`action/${key}`,
{
type: 'button',
...(action as any)
},
{
// onAction: this.handleAction,
key
// btnDisabled: store.dragging,
// data: store.getData(data)
}
)
)}
</div>
) : 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 <Table
title={this.renderSchema(title, {data: this.props.data})}
footer={this.renderSchema(footer, {data: this.props.data})}
columns={this.getColumns(store.filteredColumns)}
dataSource={sourceValue}
rowSelection={rowSelection}
expandable={expandable}
footSummary={this.getSummary(footSummary)}
headSummary={this.getSummary(headSummary)}
{...rest}>
</Table>;
}
render() {
const {
classnames: cx
} = this.props;
this.renderedToolbars = []; // 用来记录哪些 toolbar 已经渲染了
return <div className={cx('Table-wrapper')}>
{this.renderActions('header')}
{this.renderTable()}
</div>;
}
}

View File

@ -156,14 +156,16 @@ export class TableBody extends React.Component<TableBodyProps> {
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<TableBodyProps> {
}
// 缺少的单元格补齐
// 考虑是否设置了
// 开启了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, {

View File

@ -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,

185
src/store/table-v2.ts Normal file
View File

@ -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<typeof Column>;
export type SColumn = SnapshotIn<typeof Column>;
export const Row = types
.model('Row', {
data: types.frozen({} as any)
});
export type IRow = Instance<typeof Row>;
export type SRow = SnapshotIn<typeof Row>;
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<STableStore>) {
config.columnsToggable !== void 0 &&
(self.columnsToggable = config.columnsToggable);
if (config.columns && Array.isArray(config.columns)) {
let columns: Array<SColumn> = 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<typeof TableStoreV2>;
export type STableStore = SnapshotIn<typeof TableStoreV2>;

View File

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