diff --git a/docs/zh-CN/components/table.md b/docs/zh-CN/components/table.md
index b749dadf0..6e457b986 100755
--- a/docs/zh-CN/components/table.md
+++ b/docs/zh-CN/components/table.md
@@ -1747,6 +1747,16 @@ order: 67
}
```
+## 表格内容高度自适应
+
+> 1.5.0 及以上版本
+
+通过 `autoFillHeight` 可以让表格内容区自适应高度,具体效果请看这个[示例](../../../examples/crud/auto-fill)。
+
+它的展现效果是整个内容区域高度自适应,表格内容较多时在内容区域内出滚动条,这样顶部筛选和底部翻页的位置都是固定的。
+
+开启这个配置后会自动关闭 `affixHeader` 功能避免冲突。
+
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
@@ -1773,6 +1783,7 @@ order: 67
| prefixRow | `Array` | | 顶部总结行 |
| affixRow | `Array` | | 底部总结行 |
| itemBadge | [`BadgeSchema`](./badge) | | 行角标配置 |
+| autoFillHeight | `boolean` | | 内容区域自适应高度 |
## 列配置属性表
diff --git a/examples/components/CRUD/TableAutoFill.jsx b/examples/components/CRUD/TableAutoFill.jsx
new file mode 100644
index 000000000..551664fd4
--- /dev/null
+++ b/examples/components/CRUD/TableAutoFill.jsx
@@ -0,0 +1,329 @@
+export default {
+ title: '表格内容区域自适应屏幕高度,内容超出时在内容区出现滚动条',
+ remark: 'bla bla bla',
+ toolbar: [
+ {
+ type: 'button',
+ actionType: 'dialog',
+ label: '新增',
+ icon: 'fa fa-plus pull-left',
+ primary: true,
+ dialog: {
+ title: '新增',
+ body: {
+ type: 'form',
+ name: 'sample-edit-form',
+ api: 'post:/api/sample',
+ body: [
+ {
+ type: 'input-text',
+ name: 'engine',
+ label: 'Engine',
+ required: true
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'input-text',
+ name: 'browser',
+ label: 'Browser',
+ required: true
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'input-text',
+ name: 'platform',
+ label: 'Platform(s)',
+ required: true
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'input-text',
+ name: 'version',
+ label: 'Engine version'
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'input-text',
+ name: 'grade',
+ label: 'CSS grade'
+ }
+ ]
+ }
+ }
+ }
+ ],
+ body: {
+ type: 'crud',
+ draggable: true,
+ api: '/api/sample',
+ perPage: 50,
+ keepItemSelectionOnPageChange: true,
+ maxKeepItemSelectionLength: 11,
+ autoFillHeight: true,
+ labelTpl: '${id} ${engine}',
+ autoGenerateFilter: true,
+ bulkActions: [
+ {
+ label: '批量删除',
+ actionType: 'ajax',
+ api: 'delete:/api/sample/${ids|raw}',
+ confirmText: '确定要批量删除?'
+ },
+ {
+ label: '批量修改',
+ actionType: 'dialog',
+ dialog: {
+ title: '批量编辑',
+ name: 'sample-bulk-edit',
+ body: {
+ type: 'form',
+ api: '/api/sample/bulkUpdate2',
+ body: [
+ {
+ type: 'hidden',
+ name: 'ids'
+ },
+ {
+ type: 'input-text',
+ name: 'engine',
+ label: 'Engine'
+ }
+ ]
+ }
+ }
+ }
+ ],
+ quickSaveApi: '/api/sample/bulkUpdate',
+ quickSaveItemApi: '/api/sample/$id',
+ filterTogglable: true,
+ headerToolbar: [
+ 'bulkActions',
+ {
+ type: 'tpl',
+ tpl: '定制内容示例:当前有 ${count} 条数据。',
+ className: 'v-middle'
+ },
+ {
+ type: 'link',
+ href: 'https://www.baidu.com',
+ body: '百度一下',
+ htmlTarget: '_parent',
+ className: 'v-middle'
+ },
+ {
+ type: 'columns-toggler',
+ align: 'right'
+ },
+ {
+ type: 'drag-toggler',
+ align: 'right'
+ },
+ {
+ type: 'pagination',
+ align: 'right'
+ }
+ ],
+ footerToolbar: ['statistics', 'switch-per-page', 'pagination'],
+ // rowClassNameExpr: '<%= data.id == 1 ? "bg-success" : "" %>',
+ columns: [
+ {
+ name: 'id',
+ label: 'ID',
+ searchable: {
+ type: 'input-text',
+ name: 'id',
+ label: '主键',
+ placeholder: '输入id'
+ }
+ },
+ {
+ name: 'engine',
+ label: 'Rendering engine'
+ },
+ {
+ name: 'browser',
+ label: 'Browser',
+ searchable: {
+ type: 'select',
+ name: 'browser',
+ label: '浏览器',
+ placeholder: '选择浏览器',
+ options: [
+ {
+ label: 'Internet Explorer ',
+ value: 'ie'
+ },
+ {
+ label: 'AOL browser',
+ value: 'aol'
+ },
+ {
+ label: 'Firefox',
+ value: 'firefox'
+ }
+ ]
+ }
+ },
+ {
+ name: 'platform',
+ label: 'Platform(s)'
+ },
+ {
+ name: 'version',
+ label: 'Engine version',
+ searchable: {
+ type: 'input-number',
+ name: 'version',
+ label: '版本号',
+ placeholder: '输入版本号',
+ mode: 'horizontal'
+ }
+ },
+ {
+ name: 'grade',
+ label: 'CSS grade'
+ },
+ {
+ type: 'operation',
+ label: '操作',
+ width: 100,
+ buttons: [
+ {
+ type: 'button',
+ icon: 'fa fa-eye',
+ actionType: 'dialog',
+ tooltip: '查看',
+ dialog: {
+ title: '查看',
+ body: {
+ type: 'form',
+ body: [
+ {
+ type: 'static',
+ name: 'engine',
+ label: 'Engine'
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'static',
+ name: 'browser',
+ label: 'Browser'
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'static',
+ name: 'platform',
+ label: 'Platform(s)'
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'static',
+ name: 'version',
+ label: 'Engine version'
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'static',
+ name: 'grade',
+ label: 'CSS grade'
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'html',
+ html: '
添加其他 Html 片段 需要支持变量替换(todo).
'
+ }
+ ]
+ }
+ }
+ },
+ {
+ type: 'button',
+ icon: 'fa fa-pencil',
+ tooltip: '编辑',
+ actionType: 'drawer',
+ drawer: {
+ position: 'left',
+ size: 'lg',
+ title: '编辑',
+ body: {
+ type: 'form',
+ name: 'sample-edit-form',
+ api: '/api/sample/$id',
+ body: [
+ {
+ type: 'input-text',
+ name: 'engine',
+ label: 'Engine',
+ required: true
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'input-text',
+ name: 'browser',
+ label: 'Browser',
+ required: true
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'input-text',
+ name: 'platform',
+ label: 'Platform(s)',
+ required: true
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'input-text',
+ name: 'version',
+ label: 'Engine version'
+ },
+ {
+ type: 'divider'
+ },
+ {
+ type: 'select',
+ name: 'grade',
+ label: 'CSS grade',
+ options: ['A', 'B', 'C', 'D', 'X']
+ }
+ ]
+ }
+ }
+ },
+ {
+ type: 'button',
+ icon: 'fa fa-times text-danger',
+ actionType: 'ajax',
+ tooltip: '删除',
+ confirmText: '您确认要删除?',
+ api: 'delete:/api/sample/$id'
+ }
+ ],
+ toggled: true
+ }
+ ]
+ }
+};
diff --git a/examples/components/Example.tsx b/examples/components/Example.tsx
index 5886dde41..917171737 100644
--- a/examples/components/Example.tsx
+++ b/examples/components/Example.tsx
@@ -32,6 +32,7 @@ import Definitions from './Form/Definitions';
import AnchorNav from './Form/AnchorNav';
import TableCrudSchema from './CRUD/Table';
+import TableAutoFillSchema from './CRUD/TableAutoFill';
import ItemActionsSchema from './CRUD/ItemActions';
import GridCrudSchema from './CRUD/Grid';
import ListCrudSchema from './CRUD/List';
@@ -285,6 +286,11 @@ export const examples = [
path: '/examples/crud/table',
component: makeSchemaRenderer(TableCrudSchema)
},
+ {
+ label: '表格高度自适应',
+ path: '/examples/crud/auto-fill',
+ component: makeSchemaRenderer(TableAutoFillSchema)
+ },
{
label: '卡片模式',
path: '/examples/crud/grid',
diff --git a/scss/components/_table.scss b/scss/components/_table.scss
index e37d236ac..47dde7b51 100644
--- a/scss/components/_table.scss
+++ b/scss/components/_table.scss
@@ -891,6 +891,21 @@
top: 0;
left: 0;
}
+
+ &--autoFillHeight {
+ > .#{$ns}Table-contentWrap {
+ overflow-y: scroll;
+
+ > .#{$ns}Table-content table {
+ border-top: none; // 不然会导致拖动时顶部露出内容
+ }
+ > .#{$ns}Table-content table thead {
+ position: sticky; // 简单实现表头吸顶效果,不考虑 IE 11 不然太麻烦
+ top: 0;
+ z-index: 1; // 由于 badge 导致 tbody 里 tr 的 position: relative 了
+ }
+ }
+ }
}
.#{$ns}InputTable-toolbar {
diff --git a/src/renderers/CRUD.tsx b/src/renderers/CRUD.tsx
index 1ea3dd278..348689474 100644
--- a/src/renderers/CRUD.tsx
+++ b/src/renderers/CRUD.tsx
@@ -287,6 +287,11 @@ export interface CRUDCommonSchema extends BaseSchema {
* 开启查询区域,会根据列元素的searchable属性值,自动生成查询条件表单
*/
autoGenerateFilter?: boolean;
+
+ /**
+ * 内容区域占满屏幕剩余空间
+ */
+ autoFillHeight?: boolean;
}
export type CRUDCardsSchema = CRUDCommonSchema & {
@@ -366,7 +371,8 @@ export default class CRUD extends React.Component {
'onInit',
'onSaved',
'onQuery',
- 'formStore'
+ 'formStore',
+ 'autoFillHeight'
];
static defaultProps = {
toolbarInline: true,
@@ -382,7 +388,8 @@ export default class CRUD extends React.Component {
filterTogglable: false,
filterDefaultVisible: true,
loadDataOnce: false,
- loadDataOnceFetchOnFilter: true
+ loadDataOnceFetchOnFilter: true,
+ autoFillHeight: false
};
control: any;
@@ -2000,6 +2007,7 @@ export default class CRUD extends React.Component {
onQuery,
autoGenerateFilter,
onSelect,
+ autoFillHeight,
...rest
} = this.props;
@@ -2051,6 +2059,7 @@ export default class CRUD extends React.Component {
className: cx('Crud-body', bodyClassName),
ref: this.controlRef,
autoGenerateFilter: !filter && autoGenerateFilter,
+ autoFillHeight: autoFillHeight,
selectable: !!(
(this.hasBulkActionsToolbar() && this.hasBulkActions()) ||
pickerMode
diff --git a/src/renderers/Table/TableContent.tsx b/src/renderers/Table/TableContent.tsx
index 7b5e530c3..06b55636d 100644
--- a/src/renderers/Table/TableContent.tsx
+++ b/src/renderers/Table/TableContent.tsx
@@ -1,11 +1,12 @@
import React from 'react';
import {ClassNamesFn} from '../../theme';
-import {IColumn, IRow} from '../../store/table';
+import {IColumn, IRow, ITableStore} from '../../store/table';
import {SchemaNode, Action} from '../../types';
import {TableBody} from './TableBody';
import {LocaleProps} from '../../locale';
import {observer} from 'mobx-react';
import {ActionSchema} from '../Action';
+import ItemActionsWrapper from './ItemActionsWrapper';
export interface TableContentProps extends LocaleProps {
className?: string;
@@ -50,10 +51,45 @@ export interface TableContentProps extends LocaleProps {
prefixRow?: Array;
affixRow?: Array;
itemAction?: ActionSchema;
+ itemActions?: Array;
+ store: ITableStore;
}
@observer
export class TableContent extends React.Component {
+ renderItemActions() {
+ const {itemActions, render, store, classnames: cx} = this.props;
+ const finalActions = Array.isArray(itemActions)
+ ? itemActions.filter(action => !action.hiddenOnHover)
+ : [];
+
+ if (!finalActions.length) {
+ return null;
+ }
+
+ return (
+
+
+ {finalActions.map((action, index) =>
+ render(
+ `itemAction/${index}`,
+ {
+ ...(action as any),
+ isMenuItem: true
+ },
+ {
+ key: index,
+ item: store.hoverRow,
+ data: store.hoverRow!.locals,
+ rowIndex: store.hoverRow!.index
+ }
+ )
+ )}
+
+
+ );
+ }
+
render() {
const {
placeholder,
@@ -82,7 +118,8 @@ export class TableContent extends React.Component {
locale,
translate,
itemAction,
- affixRow
+ affixRow,
+ store
} = this.props;
const tableClassName = cx('Table-table', this.props.tableClassName);
@@ -94,6 +131,7 @@ export class TableContent extends React.Component {
className={cx('Table-content', className)}
onScroll={onScroll}
>
+ {store.hoverRow ? this.renderItemActions() : null}
{columnsGroup.length ? (
diff --git a/src/renderers/Table/index.tsx b/src/renderers/Table/index.tsx
index 89ccaff18..6e3d42049 100644
--- a/src/renderers/Table/index.tsx
+++ b/src/renderers/Table/index.tsx
@@ -53,9 +53,10 @@ import {TableBody} from './TableBody';
import {TplSchema} from '../Tpl';
import {MappingSchema} from '../Mapping';
import {isAlive, getSnapshot} from 'mobx-state-tree';
-import ItemActionsWrapper from './ItemActionsWrapper';
import ColumnToggler from './ColumnToggler';
import {BadgeSchema} from '../../components/Badge';
+import offset from '../../utils/offset';
+import {getStyleNumber} from '../../utils/dom';
/**
* 表格列,不指定类型时默认为文本类型。
@@ -422,7 +423,8 @@ export default class Table extends React.Component {
'headerToolbarClassName',
'toolbarClassName',
'footerToolbarClassName',
- 'itemBadge'
+ 'itemBadge',
+ 'autoFillHeight'
];
static defaultProps: Partial = {
className: '',
@@ -497,6 +499,7 @@ export default class Table extends React.Component {
this.subFormRef = this.subFormRef.bind(this);
this.handleColumnToggle = this.handleColumnToggle.bind(this);
this.renderAutoFilterForm = this.renderAutoFilterForm.bind(this);
+ this.updateAutoFillHeight = this.updateAutoFillHeight.bind(this);
const {
store,
@@ -607,6 +610,79 @@ export default class Table extends React.Component {
this.affixDetect();
parent.addEventListener('scroll', this.affixDetect);
window.addEventListener('resize', this.affixDetect);
+ this.updateAutoFillHeight();
+ window.addEventListener('resize', this.updateAutoFillHeight);
+ }
+
+ /**
+ * 自动设置表格高度占满界面剩余区域
+ * 用 css 实现有点麻烦,要改很多结构,所以先用 dom hack 了,避免对之前的功能有影响
+ */
+ updateAutoFillHeight() {
+ const {autoFillHeight, footerToolbar, classPrefix: ns} = this.props;
+ if (!autoFillHeight) {
+ return;
+ }
+ const table = findDOMNode(this) as HTMLElement;
+ const tableContent = table.querySelector(
+ `.${ns}Table-content`
+ ) as HTMLElement;
+ const tableContentWrap = table.querySelector(
+ `.${ns}Table-contentWrap`
+ ) as HTMLElement;
+ const footToolbar = table.querySelector(
+ `.${ns}Table-footToolbar`
+ ) as HTMLElement;
+ if (!tableContent) {
+ return;
+ }
+
+ // table 底部 margin
+ const tableMarginBottom = getStyleNumber(table, 'margin-bottom');
+ // 计算 table-content 在 dom 中的位置
+ const tableContentTop = offset(tableContent).top;
+ const viewportHeight = window.innerHeight;
+ // 有时候会拿不到 footToolbar?
+ const footToolbarHeight = footToolbar ? offset(footToolbar).height : 0;
+ // 有时候会拿不到 footToolbar,等一下在执行
+ if (!footToolbarHeight && footerToolbar && footerToolbar.length) {
+ setTimeout(() => {
+ this.updateAutoFillHeight();
+ }, 100);
+ return;
+ }
+ const footToolbarMarginBottom = getStyleNumber(
+ footToolbar,
+ 'margin-bottom'
+ );
+
+ const tableContentWrapMarginButtom = getStyleNumber(
+ tableContentWrap,
+ 'margin-bottom'
+ );
+
+ // 循环计算父级节点的 pddding,这里不考虑父级节点还可能会有其它兄弟节点的情况了
+ let allParentPaddingButtom = 0;
+ let parentNode = tableContent.parentElement;
+ while (parentNode) {
+ const paddingButtom = getStyleNumber(parentNode, 'padding-bottom');
+ const borderBottom = getStyleNumber(parentNode, 'border-bottom-width');
+ allParentPaddingButtom =
+ allParentPaddingButtom + paddingButtom + borderBottom;
+ parentNode = parentNode.parentElement;
+ }
+
+ tableContent.style.height = `${
+ viewportHeight -
+ tableContentTop -
+ tableContentWrapMarginButtom -
+ footToolbarHeight -
+ Math.max(
+ footToolbarMarginBottom,
+ allParentPaddingButtom,
+ tableMarginBottom
+ )
+ }px`;
}
componentDidUpdate(prevProps: TableProps) {
@@ -692,6 +768,7 @@ export default class Table extends React.Component {
const parent = this.parentNode;
parent && parent.removeEventListener('scroll', this.affixDetect);
window.removeEventListener('resize', this.affixDetect);
+ window.removeEventListener('resize', this.updateAutoFillHeight);
(this.updateTableInfoLazy as any).cancel();
this.unSensor && this.unSensor();
@@ -881,7 +958,7 @@ export default class Table extends React.Component {
}
affixDetect() {
- if (!this.props.affixHeader || !this.table) {
+ if (!this.props.affixHeader || !this.table || this.props.autoFillHeight) {
return;
}
@@ -2584,39 +2661,6 @@ export default class Table extends React.Component {
: footerNode || toolbarNode || null;
}
- renderItemActions() {
- const {itemActions, render, store, classnames: cx} = this.props;
- const finalActions = Array.isArray(itemActions)
- ? itemActions.filter(action => !action.hiddenOnHover)
- : [];
-
- if (!finalActions.length) {
- return null;
- }
-
- return (
-
-
- {finalActions.map((action, index) =>
- render(
- `itemAction/${index}`,
- {
- ...(action as any),
- isMenuItem: true
- },
- {
- key: index,
- item: store.hoverRow,
- data: store.hoverRow!.locals,
- rowIndex: store.hoverRow!.index
- }
- )
- )}
-
-
- );
- }
-
renderTableContent() {
const {
classnames: cx,
@@ -2631,8 +2675,11 @@ export default class Table extends React.Component {
prefixRow,
locale,
affixRow,
+ tableContentClassName,
translate,
- itemAction
+ itemAction,
+ autoFillHeight,
+ itemActions
} = this.props;
// 理论上来说 store.rows 应该也行啊
@@ -2645,7 +2692,10 @@ export default class Table extends React.Component {
store.combineNum > 0 ? 'Table-table--withCombine' : '',
tableClassName
)}
+ className={tableContentClassName}
+ itemActions={itemActions}
itemAction={itemAction}
+ store={store}
classnames={cx}
columns={store.filteredColumns}
columnsGroup={store.columnGroup}
@@ -2681,6 +2731,7 @@ export default class Table extends React.Component {
store,
classnames: cx,
affixColumns,
+ autoFillHeight,
autoGenerateFilter
} = this.props;
@@ -2697,7 +2748,8 @@ export default class Table extends React.Component {
return (
{autoGenerateFilter ? this.renderAutoFilterForm() : null}
@@ -2734,7 +2786,6 @@ export default class Table extends React.Component
{
: null}
{this.renderTableContent()}
- {store.hoverRow ? this.renderItemActions() : null}
{this.renderAffixHeader(tableClassName)}
{footer}
diff --git a/src/utils/api.ts b/src/utils/api.ts
index fbb2b86c2..3bdbc3fe5 100644
--- a/src/utils/api.ts
+++ b/src/utils/api.ts
@@ -198,7 +198,7 @@ export function responseAdaptor(ret: fetcherResult, api: ApiObject) {
let hasStatusField = true;
if (!data) {
- throw new Error('Response is empty!');
+ throw new Error('Response is empty');
}
// 兼容几种常见写法
diff --git a/src/utils/dom.tsx b/src/utils/dom.tsx
index e51cd5743..9cd92a1db 100644
--- a/src/utils/dom.tsx
+++ b/src/utils/dom.tsx
@@ -267,3 +267,15 @@ export function calculatePosition(
activePlacement
};
}
+
+/**
+ * 专门用来获取样式的像素值,默认返回 0
+ */
+export function getStyleNumber(element: HTMLElement, styleName: string) {
+ if (!element) {
+ return 0;
+ }
+ return (
+ parseInt(getComputedStyle(element).getPropertyValue(styleName), 10) || 0
+ );
+}