feat: 添加 tabs-transfer-picker 组件 (#2972)

This commit is contained in:
liaoxuezhi 2021-11-17 18:03:07 +08:00 committed by GitHub
parent 019dac8040
commit dd8229a938
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 751 additions and 537 deletions

View File

@ -0,0 +1,126 @@
---
title: TabsTransferPicker 穿梭选择器
description:
type: 0
group: null
menuName: TabsTransferPicker 穿梭选择器
icon:
---
在[TabsTransfer 组合穿梭器](./tabs-transfer)的基础上扩充了弹窗选择模式,展示值用的是简单的 input 框,但是编辑的操作是弹窗个穿梭框来完成。
适合用来做复杂选人组件。
```schema: scope="body"
{
"type": "form",
"api": "/api/mock2/form/saveForm",
"body": [
{
"label": "组合穿梭器",
"type": "tabs-transfer-picker",
"name": "a",
"sortable": true,
"selectMode": "tree",
"searchable": true,
"pickerSize": "md",
"options": [
{
"label": "成员",
"selectMode": "tree",
"children": [
{
"label": "法师",
"children": [
{
"label": "诸葛亮",
"value": "zhugeliang"
}
]
},
{
"label": "战士",
"children": [
{
"label": "曹操",
"value": "caocao"
},
{
"label": "钟无艳",
"value": "zhongwuyan"
}
]
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai"
},
{
"label": "韩信",
"value": "hanxin"
},
{
"label": "云中君",
"value": "yunzhongjun"
}
]
}
]
},
{
"label": "用户",
"selectMode": "chained",
"children": [
{
"label": "法师",
"children": [
{
"label": "诸葛亮",
"value": "zhugeliang"
}
]
},
{
"label": "战士",
"children": [
{
"label": "曹操",
"value": "caocao"
},
{
"label": "钟无艳",
"value": "zhongwuyan"
}
]
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai"
},
{
"label": "韩信",
"value": "hanxin"
},
{
"label": "云中君",
"value": "yunzhongjun"
}
]
}
]
}
]
}
]
}
```
## 属性表
更多配置请参考[TabsTransfer 组合穿梭器](./tabs-transfer)。

View File

@ -23,92 +23,41 @@ icon:
"searchable": true, "searchable": true,
"options": [ "options": [
{ {
"label": "成员", "label": "法师",
"selectMode": "tree",
"children": [ "children": [
{ {
"label": "法师", "label": "诸葛亮",
"children": [ "value": "zhugeliang"
{
"label": "诸葛亮",
"value": "zhugeliang"
}
]
},
{
"label": "战士",
"children": [
{
"label": "曹操",
"value": "caocao"
},
{
"label": "钟无艳",
"value": "zhongwuyan"
}
]
},
{
"label": "打野",
"children": [
{
"label": "李白",
"value": "libai"
},
{
"label": "韩信",
"value": "hanxin"
},
{
"label": "云中君",
"value": "yunzhongjun"
}
]
} }
] ]
}, },
{ {
"label": "用户", "label": "战士",
"selectMode": "chained",
"children": [ "children": [
{ {
"label": "法师", "label": "曹操",
"children": [ "value": "caocao"
{
"label": "诸葛亮",
"value": "zhugeliang2"
}
]
}, },
{ {
"label": "战士", "label": "钟无艳",
"children": [ "value": "zhongwuyan"
{ }
"label": "曹操", ]
"value": "caocao2" },
}, {
{ "label": "打野",
"label": "钟无艳", "children": [
"value": "zhongwuyan2" {
} "label": "李白",
] "value": "libai"
}, },
{ {
"label": "打野", "label": "韩信",
"children": [ "value": "hanxin"
{ },
"label": "李白", {
"value": "libai2" "label": "云中君",
}, "value": "yunzhongjun"
{
"label": "韩信",
"value": "hanxin2"
},
{
"label": "云中君",
"value": "yunzhongjun2"
}
]
} }
] ]
} }

View File

@ -34,6 +34,7 @@ order: 60
} }
} }
``` ```
## 配置工具栏 ## 配置工具栏
```schema: scope="body" ```schema: scope="body"
@ -106,7 +107,7 @@ order: 60
## 隐藏头部 ## 隐藏头部
去掉头部默认只展示内容tab第一项的内容 去掉头部,默认只展示内容 tab 第一项的内容
```schema: scope="body" ```schema: scope="body"
{ {
@ -121,9 +122,9 @@ order: 60
} }
``` ```
## 设置style ## 设置 style
默认tabs只有一项的时候没有选中状态 默认 tabs 只有一项的时候没有选中状态
```schema: scope="body" ```schema: scope="body"
{ {
@ -140,8 +141,6 @@ order: 60
} }
``` ```
## 去掉分隔线 ## 去掉分隔线
```schema: scope="body" ```schema: scope="body"
@ -157,9 +156,9 @@ order: 60
} }
``` ```
## source动态数据 ## source 动态数据
配置 source 属性,根据某个数据来动态生成。具体使用参考Tabs选项卡组件 配置 source 属性,根据某个数据来动态生成。具体使用参考 Tabs 选项卡组件
## 图标 ## 图标
@ -215,26 +214,26 @@ order: 60
## 属性表 ## 属性表
| 属性名 | 类型 | 默认值 | 说明 | | 属性名 | 类型 | 默认值 | 说明 |
| --------------------- | --------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------ | | --------------------- | ------------------------------------ | ----------------------------------- | ------------------------------------------------------------------------------------------ |
| type | `string` | `"portlet"` | 指定为 Portlet 渲染器 | | type | `string` | `"portlet"` | 指定为 Portlet 渲染器 |
| className | `string` | | 外层 Dom 的类名 | | className | `string` | | 外层 Dom 的类名 |
| tabsClassName | `string` | | Tabs Dom 的类名 | | tabsClassName | `string` | | Tabs Dom 的类名 |
| contentClassName | `string` | | Tabs content Dom 的类名 | | contentClassName | `string` | | Tabs content Dom 的类名 |
| tabs | `Array` | | tabs 内容 | | tabs | `Array` | | tabs 内容 |
| source | `Object` | | tabs 关联数据,关联后可以重复生成选项卡 | | source | `Object` | | tabs 关联数据,关联后可以重复生成选项卡 |
| toolbar | [SchemaNode](../types/schemanode) | | tabs 中的工具栏不随tab切换而变化 | | toolbar | [SchemaNode](../types/schemanode) | | tabs 中的工具栏,不随 tab 切换而变化 |
| style | `string \| Object` | | 自定义样式| | style | `string \| Object` | | 自定义样式 |
| description | [模板](../../docs/concepts/template)| | 标题右侧信息 | | description | [模板](../../docs/concepts/template) | | 标题右侧信息 |
| hideHeader | `boolean` | false | 隐藏头部 | | hideHeader | `boolean` | false | 隐藏头部 |
| divider | `boolean` | false | 去掉分隔线 | | divider | `boolean` | false | 去掉分隔线 |
| tabs[x].title | `string` | | Tab 标题 | | tabs[x].title | `string` | | Tab 标题 |
| tabs[x].icon | `icon` | | Tab 的图标 | | tabs[x].icon | `icon` | | Tab 的图标 |
| tabs[x].tab | [SchemaNode](../types/schemanode) | | 内容区 | | tabs[x].tab | [SchemaNode](../types/schemanode) | | 内容区 |
| tabs[x].toolbar | [SchemaNode](../types/schemanode) | | tabs 中的工具栏随tab切换而变化 | tabs[x].toolbar | [SchemaNode](../types/schemanode) | | tabs 中的工具栏,随 tab 切换而变化 |
| tabs[x].reload | `boolean` | | 设置以后内容每次都会重新渲染,对于 crud 的重新拉取很有用 | | tabs[x].reload | `boolean` | | 设置以后内容每次都会重新渲染,对于 crud 的重新拉取很有用 |
| tabs[x].unmountOnExit | `boolean` | | 每次退出都会销毁当前 tab 栏内容 | | tabs[x].unmountOnExit | `boolean` | | 每次退出都会销毁当前 tab 栏内容 |
| tabs[x].className | `string` | `"bg-white b-l b-r b-b wrapper-md"` | Tab 区域样式 | | tabs[x].className | `string` | `"bg-white b-l b-r b-b wrapper-md"` | Tab 区域样式 |
| mountOnEnter | `boolean` | false | 只有在点中 tab 的时候才渲染 | | mountOnEnter | `boolean` | false | 只有在点中 tab 的时候才渲染 |
| unmountOnExit | `boolean` | false | 切换 tab 的时候销毁 | | unmountOnExit | `boolean` | false | 切换 tab 的时候销毁 |
| scrollable | `boolean` | false | 是否导航支持内容溢出滚动,`vertical`和`chrome`模式下不支持该属性;`chrome`模式默认压缩标签 | | scrollable | `boolean` | false | 是否导航支持内容溢出滚动,`vertical`和`chrome`模式下不支持该属性;`chrome`模式默认压缩标签 |

View File

@ -658,6 +658,14 @@ export const components = [
makeMarkdownRenderer makeMarkdownRenderer
) )
}, },
{
label: 'TransferPicker 穿梭选择器',
path: '/zh-CN/components/form/transfer-picker',
getComponent: () =>
import('../../docs/zh-CN/components/form/transfer-picker.md').then(
makeMarkdownRenderer
)
},
{ {
label: 'TabsTransfer 组合穿梭器', label: 'TabsTransfer 组合穿梭器',
path: '/zh-CN/components/form/tabs-transfer', path: '/zh-CN/components/form/tabs-transfer',
@ -667,12 +675,12 @@ export const components = [
) )
}, },
{ {
label: 'TransferPicker 穿梭选择器', label: 'TabsTransferPicker 组合穿梭选择器',
path: '/zh-CN/components/form/transfer-picker', path: '/zh-CN/components/form/tabs-transfer-picker',
getComponent: () => getComponent: () =>
import('../../docs/zh-CN/components/form/transfer-picker.md').then( import(
makeMarkdownRenderer '../../docs/zh-CN/components/form/tabs-transfer-picker.md'
) ).then(makeMarkdownRenderer)
}, },
{ {
label: 'InputTree 树形选择框', label: 'InputTree 树形选择框',

View File

@ -111,6 +111,7 @@ import {TreeSelectControlSchema} from './renderers/Form/TreeSelect';
import {UUIDControlSchema} from './renderers/Form/UUID'; import {UUIDControlSchema} from './renderers/Form/UUID';
import {FormControlSchema} from './renderers/Form/Control'; import {FormControlSchema} from './renderers/Form/Control';
import {TransferPickerControlSchema} from './renderers/Form/TransferPicker'; import {TransferPickerControlSchema} from './renderers/Form/TransferPicker';
import {TabsTransferPickerControlSchema} from './renderers/Form/TabsTransferPicker';
// 每加个类型,这补充一下。 // 每加个类型,这补充一下。
export type SchemaType = export type SchemaType =
@ -311,6 +312,7 @@ export type SchemaType =
| 'textarea' | 'textarea'
| 'transfer' | 'transfer'
| 'transfer-picker' | 'transfer-picker'
| 'tabs-transfer-picker'
| 'input-tree' | 'input-tree'
| 'tree-select' | 'tree-select'
| 'table-view' | 'table-view'
@ -435,6 +437,7 @@ export type SchemaObject =
| TextareaControlSchema | TextareaControlSchema
| TransferControlSchema | TransferControlSchema
| TransferPickerControlSchema | TransferPickerControlSchema
| TabsTransferPickerControlSchema
| TreeControlSchema | TreeControlSchema
| TreeSelectControlSchema; | TreeSelectControlSchema;

View File

@ -0,0 +1,85 @@
import {localeable} from '../locale';
import {themeable} from '../theme';
import {uncontrollable} from 'uncontrollable';
import React from 'react';
import ResultBox from './ResultBox';
import {Icon} from './icons';
import PickerContainer from './PickerContainer';
import {autobind} from '../utils/helper';
import TabsTransfer, {TabsTransferProps} from './TabsTransfer';
export interface TabsTransferPickerProps
extends Omit<TabsTransferProps, 'itemRender'> {
// 新的属性?
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
}
export class TransferPicker extends React.Component<TabsTransferPickerProps> {
@autobind
handleClose() {
this.setState({
inputValue: '',
searchResult: null
});
}
@autobind
handleConfirm(value: any) {
this.props.onChange?.(value);
this.handleClose();
}
render() {
const {
classnames: cx,
value,
translate: __,
disabled,
className,
onChange,
size,
...rest
} = this.props;
return (
<PickerContainer
title={__('Select.placeholder')}
popOverRender={({onClose, value, onChange}) => {
return <TabsTransfer {...rest} value={value} onChange={onChange} />;
}}
value={value}
onConfirm={this.handleConfirm}
onCancel={this.handleClose}
size={size}
>
{({onClick, isOpened}) => (
<ResultBox
className={cx(
'TransferPicker',
className,
isOpened ? 'is-active' : ''
)}
allowInput={false}
result={value}
onResultChange={onChange}
onResultClick={onClick}
placeholder={__('Select.placeholder')}
disabled={disabled}
>
<span className={cx('TransferPicker-icon')}>
<Icon icon="pencil" className="icon" />
</span>
</ResultBox>
)}
</PickerContainer>
);
}
}
export default themeable(
localeable(
uncontrollable(TransferPicker, {
value: 'onChange'
})
)
);

View File

@ -1,46 +1,19 @@
import {localeable} from '../locale'; import {localeable} from '../locale';
import {themeable} from '../theme'; import {themeable} from '../theme';
import {Transfer, TransferProps} from './Transfer'; import Transfer, {TransferProps} from './Transfer';
import {uncontrollable} from 'uncontrollable'; import {uncontrollable} from 'uncontrollable';
import React from 'react'; import React from 'react';
import ResultBox from './ResultBox'; import ResultBox from './ResultBox';
import {Icon} from './icons'; import {Icon} from './icons';
import PickerContainer from './PickerContainer'; import PickerContainer from './PickerContainer';
import InputBox from './InputBox'; import {autobind} from '../utils/helper';
import {BaseSelection} from './Selection';
import {autobind, flattenTree} from '../utils/helper';
import ResultList from './ResultList';
import {Options} from './Select';
export interface TransferPickerProps extends TransferProps { export interface TransferPickerProps extends Omit<TransferProps, 'itemRender'> {
// 新的属性? // 新的属性?
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full'; size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
} }
export class TransferPicker extends Transfer<TransferPickerProps> { export class TransferPicker extends React.Component<TransferPickerProps> {
handlePickerToggleAll(value: any, onChange: (value: any) => void) {
const {options, option2value} = this.props;
let valueArray = BaseSelection.value2array(value, options, option2value);
const availableOptions = flattenTree(options).filter(
(option, index, list) =>
!option.disabled &&
option.value !== void 0 &&
list.indexOf(option) === index
);
if (valueArray.length < availableOptions.length) {
valueArray = availableOptions;
} else {
valueArray = [];
}
let newValue: string | Options = option2value
? valueArray.map(item => option2value(item))
: valueArray;
onChange && onChange(newValue);
}
@autobind @autobind
handleClose() { handleClose() {
this.setState({ this.setState({
@ -63,92 +36,15 @@ export class TransferPicker extends Transfer<TransferPickerProps> {
disabled, disabled,
className, className,
onChange, onChange,
onSearch, size,
options, ...rest
option2value,
inline,
showArrow,
resultTitle,
statistics,
sortable,
resultItemRender,
size
} = this.props; } = this.props;
return ( return (
<PickerContainer <PickerContainer
title={__('Select.placeholder')} title={__('Select.placeholder')}
popOverRender={({onClose, value, onChange}) => { popOverRender={({onClose, value, onChange}) => {
this.valueArray = BaseSelection.value2array( return <Transfer {...rest} value={value} onChange={onChange} />;
value,
options,
option2value
);
this.availableOptions = flattenTree(options).filter(
(option, index, list) =>
!option.disabled &&
option.value !== void 0 &&
list.indexOf(option) === index
);
return (
<>
<div
className={cx(
'Transfer',
className,
inline ? 'Transfer--inline' : ''
)}
>
<div className={cx('Transfer-select')}>
{this.renderSelect({
...this.props,
value,
onChange,
onToggleAll: () =>
this.handlePickerToggleAll(value, onChange)
})}
</div>
<div className={cx('Transfer-mid')}>
{showArrow /*todo 需要改成确认模式,即:点了按钮才到右边 */ ? (
<div className={cx('Transfer-arrow')}>
<Icon icon="right-arrow" className="icon" />
</div>
) : null}
</div>
<div className={cx('Transfer-result')}>
<div className={cx('Transfer-title')}>
<span>
{__(resultTitle || 'Transfer.selectd')}
{statistics !== false ? (
<span>
{this.valueArray.length}/
{this.availableOptions.length}
</span>
) : null}
</span>
<a
onClick={this.clearAll}
className={cx(
'Transfer-clearAll',
disabled || !this.valueArray.length ? 'is-disabled' : ''
)}
>
{__('clear')}
</a>
</div>
<ResultList
className={cx('Transfer-selections')}
sortable={sortable}
disabled={disabled}
value={value}
onChange={onChange}
placeholder={__('Transfer.selectFromLeft')}
itemRender={resultItemRender}
/>
</div>
</div>
</>
);
}} }}
value={value} value={value}
onConfirm={this.handleConfirm} onConfirm={this.handleConfirm}

View File

@ -118,6 +118,7 @@ import './renderers/Form/IconPicker';
import './renderers/Form/Formula'; import './renderers/Form/Formula';
import './renderers/Form/FieldSet'; import './renderers/Form/FieldSet';
import './renderers/Form/TabsTransfer'; import './renderers/Form/TabsTransfer';
import './renderers/Form/TabsTransferPicker';
import './renderers/Form/Group'; import './renderers/Form/Group';
import './renderers/Form/InputGroup'; import './renderers/Form/InputGroup';
import './renderers/Grid'; import './renderers/Grid';

View File

@ -0,0 +1,123 @@
import {
OptionsControlProps,
OptionsControl,
FormOptionsControl
} from './Options';
import React from 'react';
import {Api} from '../../types';
import Spinner from '../../components/Spinner';
import {BaseTransferRenderer} from './Transfer';
import TabsTransfer from '../../components/TabsTransfer';
import {SchemaApi} from '../../Schema';
import TransferPicker from '../../components/TransferPicker';
import TabsTransferPicker from '../../components/TabsTransferPicker';
/**
* TabsTransferPicker 穿
* https://baidu.gitee.io/amis/docs/components/form/tabs-transfer-picker
*/
export interface TabsTransferPickerControlSchema extends FormOptionsControl {
type: 'tabs-transfer-picker';
/**
*
*/
showArrow?: boolean;
/**
*
*/
sortable?: boolean;
/**
*
*/
searchResultMode?: 'table' | 'list' | 'tree' | 'chained';
/**
*
*/
searchable?: boolean;
/**
* API
*/
searchApi?: SchemaApi;
/**
*
*/
selectTitle?: string;
/**
*
*/
resultTitle?: string;
/**
*
*/
pickerSize?: 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full';
}
export interface TabsTransferProps
extends OptionsControlProps,
Omit<
TabsTransferPickerControlSchema,
| 'type'
| 'options'
| 'inputClassName'
| 'className'
| 'descriptionClassName'
> {}
@OptionsControl({
type: 'tabs-transfer-picker'
})
export class TabsTransferPickerRenderer extends BaseTransferRenderer<TabsTransferProps> {
render() {
const {
className,
classnames: cx,
options,
selectedOptions,
sortable,
loading,
searchable,
searchResultMode,
showArrow,
deferLoad,
disabled,
selectTitle,
resultTitle,
pickerSize,
columns,
leftMode,
leftOptions
} = this.props;
return (
<div className={cx('TabsTransferControl', className)}>
<TabsTransferPicker
value={selectedOptions}
disabled={disabled}
options={options}
onChange={this.handleChange}
option2value={this.option2value}
sortable={sortable}
searchResultMode={searchResultMode}
onSearch={searchable ? this.handleSearch : undefined}
showArrow={showArrow}
onDeferLoad={deferLoad}
selectTitle={selectTitle}
resultTitle={resultTitle}
size={pickerSize}
leftMode={leftMode}
leftOptions={leftOptions}
/>
<Spinner overlay key="info" show={loading} />
</div>
);
}
}

View File

@ -89,7 +89,10 @@ export class TransferPickerRenderer extends BaseTransferRenderer<TabsTransferPro
disabled, disabled,
selectTitle, selectTitle,
resultTitle, resultTitle,
pickerSize pickerSize,
columns,
leftMode,
leftOptions
} = this.props; } = this.props;
return ( return (
@ -108,6 +111,9 @@ export class TransferPickerRenderer extends BaseTransferRenderer<TabsTransferPro
selectTitle={selectTitle} selectTitle={selectTitle}
resultTitle={resultTitle} resultTitle={resultTitle}
size={pickerSize} size={pickerSize}
columns={columns}
leftMode={leftMode}
leftOptions={leftOptions}
/> />
<Spinner overlay key="info" show={loading} /> <Spinner overlay key="info" show={loading} />

View File

@ -6,15 +6,21 @@ import {Renderer, RendererProps} from '../factory';
import {resolveVariable} from '../utils/tpl-builtin'; import {resolveVariable} from '../utils/tpl-builtin';
import {str2AsyncFunction} from '../utils/api'; import {str2AsyncFunction} from '../utils/api';
import { import {
isVisible, isVisible,
autobind, autobind,
isDisabled, isDisabled,
isObject, isObject,
createObject createObject
} from '../utils/helper'; } from '../utils/helper';
import {filter} from '../utils/tpl'; import {filter} from '../utils/tpl';
import {SchemaTpl, SchemaClassName, BaseSchema, SchemaCollection, SchemaIcon} from '../Schema'; import {
SchemaTpl,
SchemaClassName,
BaseSchema,
SchemaCollection,
SchemaIcon
} from '../Schema';
import {ActionSchema} from './Action'; import {ActionSchema} from './Action';
@ -23,314 +29,298 @@ import {ActionSchema} from './Action';
* https://baidu.gitee.io/amis/docs/components/portlet * https://baidu.gitee.io/amis/docs/components/portlet
*/ */
export interface PortletTabSchema extends Omit<BaseSchema, 'type'> { export interface PortletTabSchema extends Omit<BaseSchema, 'type'> {
/** /**
* Tab * Tab
*/ */
title?: string; title?: string;
/** /**
* *
* @deprecated body * @deprecated body
*/ */
tab?: SchemaCollection; tab?: SchemaCollection;
/** /**
* tab切换而切换 * tab切换而切换
*/ */
toolbar?: Array<ActionSchema>; toolbar?: Array<ActionSchema>;
/** /**
* *
*/ */
body?: SchemaCollection; body?: SchemaCollection;
/** /**
* *
*/ */
icon?: SchemaIcon; icon?: SchemaIcon;
iconPosition?: 'left' | 'right'; iconPosition?: 'left' | 'right';
/** /**
* *
*/ */
reload?: boolean; reload?: boolean;
/** /**
* *
*/ */
mountOnEnter?: boolean; mountOnEnter?: boolean;
/** /**
* *
*/ */
unmountOnExit?: boolean; unmountOnExit?: boolean;
} }
export interface PortletSchema extends Omit<BaseSchema, 'type'> { export interface PortletSchema extends Omit<BaseSchema, 'type'> {
/** /**
* portlet * portlet
*/ */
type: 'portlet'; type: 'portlet';
tabs: Array<PortletTabSchema>; tabs: Array<PortletTabSchema>;
/** /**
* *
*/ */
source?: string; source?: string;
/** /**
* *
*/ */
tabsClassName?: SchemaClassName; tabsClassName?: SchemaClassName;
/** /**
* *
*/ */
tabsMode?: '' | 'line' | 'card' | 'radio' | 'vertical' | 'tiled'; tabsMode?: '' | 'line' | 'card' | 'radio' | 'vertical' | 'tiled';
/** /**
* *
*/ */
contentClassName?: SchemaClassName; contentClassName?: SchemaClassName;
/** /**
* *
*/ */
linksClassName?: SchemaClassName; linksClassName?: SchemaClassName;
/** /**
* *
*/ */
mountOnEnter?: boolean; mountOnEnter?: boolean;
/** /**
* *
*/ */
unmountOnExit?: boolean; unmountOnExit?: boolean;
/** /**
* tab切换 * tab切换
*/ */
toolbar?: Array<ActionSchema>; toolbar?: Array<ActionSchema>;
/** /**
* *
*/ */
scrollable?: boolean; scrollable?: boolean;
/** /**
* header和内容是否展示分割线 * header和内容是否展示分割线
*/ */
divider?: boolean; divider?: boolean;
/** /**
* *
*/ */
description?: SchemaTpl; description?: SchemaTpl;
/** /**
* *
*/ */
hideHeader?: boolean; hideHeader?: boolean;
/** /**
* *
*/ */
style?: string | { style?:
| string
| {
[propName: string]: any; [propName: string]: any;
}; };
} }
export interface PortletProps export interface PortletProps
extends RendererProps, extends RendererProps,
Omit<PortletSchema, 'className' | 'contentClassName'>{ Omit<PortletSchema, 'className' | 'contentClassName'> {
activeKey?: number; activeKey?: number;
tabRender?: (tab: PortletTabSchema, props: PortletProps, index: number) => JSX.Element; tabRender?: (
tab: PortletTabSchema,
props: PortletProps,
index: number
) => JSX.Element;
} }
export interface PortletState { export interface PortletState {
activeKey?: number; activeKey?: number;
} }
export class Portlet extends React.Component<PortletProps, PortletState> { export class Portlet extends React.Component<PortletProps, PortletState> {
static defaultProps: Partial<PortletProps> = { static defaultProps: Partial<PortletProps> = {
className: '', className: '',
mode: 'line', mode: 'line',
divider: true divider: true
};
renderTab?: (
tab: PortletTabSchema,
props: PortletProps,
index: number
) => JSX.Element;
constructor(props: PortletProps) {
super(props);
const activeKey = props.activeKey || 0;
this.state = {
activeKey
}; };
renderTab?: (tab: PortletTabSchema, props: PortletProps, index: number) => JSX.Element; }
constructor(props: PortletProps) {
super(props);
const activeKey = props.activeKey || 0; @autobind
handleSelect(key: number) {
this.state = { const {onSelect, tabs} = this.props;
activeKey if (typeof key === 'number' && key < tabs.length) {
}; this.setState({
activeKey: key
});
} }
@autobind if (typeof onSelect === 'string') {
handleSelect(key: number) { const selectFunc = str2AsyncFunction(onSelect, 'key', 'props');
const {onSelect, tabs} = this.props; selectFunc && selectFunc(key, this.props);
if (typeof key === 'number' && key < tabs.length) { } else if (typeof onSelect === 'function') {
this.setState({ onSelect(key, this.props);
activeKey: key }
}); }
}
if (typeof onSelect === 'string') { renderToolbarItem(toolbar: Array<ActionSchema>) {
const selectFunc = str2AsyncFunction(onSelect, 'key', 'props'); const {render} = this.props;
selectFunc && selectFunc(key, this.props); let actions: Array<JSX.Element> = [];
} else if (typeof onSelect === 'function') { if (Array.isArray(toolbar)) {
onSelect(key, this.props); toolbar.forEach((action, index) =>
} actions.push(
render(
`toolbar/${index}`,
{
type: 'button',
level: 'link',
size: 'sm',
...(action as any)
},
{
key: index
}
)
)
);
}
return actions;
}
renderToolbar() {
const {toolbar, classnames: cx, classPrefix: ns, tabs} = this.props;
const activeKey = this.state.activeKey;
let tabToolbar = null;
let tabToolbarTpl = null;
// tabs里的toolbar
const toolbarTpl = toolbar ? (
<div className={cx(`${ns}toolbar`)}>
{this.renderToolbarItem(toolbar)}
</div>
) : null;
// tab里的toolbar
if (typeof activeKey !== 'undefined') {
tabToolbar = tabs[activeKey]!.toolbar;
tabToolbarTpl = tabToolbar ? (
<div className={cx(`${ns}tab-toolbar`)}>
{this.renderToolbarItem(tabToolbar)}
</div>
) : null;
} }
renderToolbarItem(toolbar: Array<ActionSchema>) { return toolbarTpl || tabToolbarTpl ? (
const {render} = this.props; <div className={cx(`${ns}Portlet-toolbar`)}>
let actions: Array<JSX.Element> = [] {toolbarTpl}
if (Array.isArray(toolbar)) { {tabToolbarTpl}
toolbar.forEach((action, index) => </div>
actions.push( ) : null;
render( }
`toolbar/${index}`,
{ renderDesc() {
type: 'button', const {
level: 'link', description: descTpl,
size: 'sm', render,
...(action as any) classnames: cx,
}, classPrefix: ns,
{ data
key: index } = this.props;
} const desc = filter(descTpl, data);
) return desc ? (
) <span className={cx(`${ns}Portlet-header-desc`)}>{desc}</span>
); ) : null;
} }
return actions;
renderTabs() {
const {
classnames: cx,
classPrefix: ns,
tabsClassName,
contentClassName,
linksClassName,
tabRender,
render,
data,
mode: dMode,
tabsMode,
unmountOnExit,
source,
mountOnEnter,
scrollable,
divider
} = this.props;
const mode = tabsMode || dMode;
const arr = resolveVariable(source, data);
let tabs = this.props.tabs;
if (!tabs) {
return null;
} }
renderToolbar() { tabs = Array.isArray(tabs) ? tabs : [tabs];
const {toolbar, classnames: cx, classPrefix: ns, tabs} = this.props; let children: Array<JSX.Element | null> = [];
const activeKey = this.state.activeKey;
let tabToolbar = null;
let tabToolbarTpl = null;
// tabs里的toolbar
const toolbarTpl = toolbar ? (
<div className={cx(`${ns}toolbar`)}>
{this.renderToolbarItem(toolbar)}
</div>
) : null;
// tab里的toolbar const tabClassname = cx(`${ns}Portlet-tab`, tabsClassName, {
if (typeof activeKey !== 'undefined') { ['unactive-select']: tabs.length <= 1,
tabToolbar = tabs[activeKey]!.toolbar; ['no-divider']: !divider
tabToolbarTpl = tabToolbar ? ( });
<div className={cx(`${ns}tab-toolbar`)}> if (Array.isArray(arr)) {
{this.renderToolbarItem(tabToolbar)} arr.forEach((value, index) => {
</div> const ctx = createObject(
) : null; data,
} isObject(value) ? {index, ...value} : {item: value, index}
return (
toolbarTpl || tabToolbarTpl
? (<div className={cx(`${ns}Portlet-toolbar`)}>
{toolbarTpl}
{tabToolbarTpl}
</div>)
: null
); );
}
renderDesc() { children.push(
const {description : descTpl, render, classnames: cx, classPrefix: ns, data} = this.props; ...tabs.map((tab, tabIndex) =>
const desc = filter(descTpl, data); isVisible(tab, ctx) ? (
return desc
? <span className={cx(`${ns}Portlet-header-desc`)}>{desc}</span>
: null;
}
renderTabs() {
const {
classnames: cx,
classPrefix: ns,
tabsClassName,
contentClassName,
linksClassName,
tabRender,
render,
data,
mode: dMode,
tabsMode,
unmountOnExit,
source,
mountOnEnter,
scrollable,
divider
} = this.props;
const mode = tabsMode || dMode;
const arr = resolveVariable(source, data);
let tabs = this.props.tabs;
if (!tabs) {
return null;
}
tabs = Array.isArray(tabs) ? tabs : [tabs];
let children: Array<JSX.Element | null> = [];
const tabClassname = cx(`${ns}Portlet-tab`, tabsClassName, {
['unactive-select']: tabs.length <=1,
['no-divider']: !divider
});
if (Array.isArray(arr)) {
arr.forEach((value, index) => {
const ctx = createObject(
data,
isObject(value) ? {index, ...value} : {item: value, index}
);
children.push(
...tabs.map((tab, tabIndex) =>
isVisible(tab, ctx) ? (
<Tab
{...(tab as any)}
title={filter(tab.title, ctx)}
disabled={isDisabled(tab, ctx)}
key={`${index * 1000 + tabIndex}`}
eventKey={index * 1000 + tabIndex}
mountOnEnter={mountOnEnter}
unmountOnExit={
typeof tab.reload === 'boolean'
? tab.reload
: typeof tab.unmountOnExit === 'boolean'
? tab.unmountOnExit
: unmountOnExit
}
>
{render(
`item/${index}/${tabIndex}`,
(tab as any)?.type ? (tab as any) : tab.tab || tab.body,
{
data: ctx
}
)}
</Tab>
) : null
)
);
});
} else {
children = tabs.map((tab, index) =>
isVisible(tab, data) ? (
<Tab <Tab
{...(tab as any)} {...(tab as any)}
title={filter(tab.title, data)} title={filter(tab.title, ctx)}
disabled={isDisabled(tab, data)} disabled={isDisabled(tab, ctx)}
key={index} key={`${index * 1000 + tabIndex}`}
eventKey={index} eventKey={index * 1000 + tabIndex}
mountOnEnter={mountOnEnter} mountOnEnter={mountOnEnter}
unmountOnExit={ unmountOnExit={
typeof tab.reload === 'boolean' typeof tab.reload === 'boolean'
@ -340,66 +330,94 @@ export class Portlet extends React.Component<PortletProps, PortletState> {
: unmountOnExit : unmountOnExit
} }
> >
{this.renderTab {render(
? this.renderTab(tab, this.props, index) `item/${index}/${tabIndex}`,
: tabRender (tab as any)?.type ? (tab as any) : tab.tab || tab.body,
? tabRender(tab, this.props, index) {
: render( data: ctx
`tab/${index}`, }
(tab as any)?.type ? (tab as any) : tab.tab || tab.body )}
)}
</Tab> </Tab>
) : null ) : null
); )
}
return (
<CTabs
classPrefix={ns}
classnames={cx}
mode={mode}
className={tabClassname}
contentClassName={contentClassName}
linksClassName={linksClassName}
activeKey={this.state.activeKey}
onSelect={this.handleSelect}
toolbar={this.renderToolbar()}
additionBtns={this.renderDesc()}
scrollable={scrollable}
>
{children}
</CTabs>
); );
});
} else {
children = tabs.map((tab, index) =>
isVisible(tab, data) ? (
<Tab
{...(tab as any)}
title={filter(tab.title, data)}
disabled={isDisabled(tab, data)}
key={index}
eventKey={index}
mountOnEnter={mountOnEnter}
unmountOnExit={
typeof tab.reload === 'boolean'
? tab.reload
: typeof tab.unmountOnExit === 'boolean'
? tab.unmountOnExit
: unmountOnExit
}
>
{this.renderTab
? this.renderTab(tab, this.props, index)
: tabRender
? tabRender(tab, this.props, index)
: render(
`tab/${index}`,
(tab as any)?.type ? (tab as any) : tab.tab || tab.body
)}
</Tab>
) : null
);
} }
render() { return (
const { <CTabs
className, classPrefix={ns}
data, classnames={cx}
classnames: cx, mode={mode}
classPrefix: ns, className={tabClassname}
style, contentClassName={contentClassName}
hideHeader linksClassName={linksClassName}
} = this.props; activeKey={this.state.activeKey}
const portletClassname = cx(`${ns}Portlet`, className, { onSelect={this.handleSelect}
['no-header']: hideHeader toolbar={this.renderToolbar()}
}); additionBtns={this.renderDesc()}
const styleVar = scrollable={scrollable}
typeof style === 'string' >
? resolveVariable(style, data) || {} {children}
: mapValues(style, s => resolveVariable(s, data) || s); </CTabs>
);
}
return ( render() {
<div className={portletClassname} style={styleVar}> const {
{this.renderTabs()} className,
</div> data,
) classnames: cx,
} classPrefix: ns,
style,
hideHeader
} = this.props;
const portletClassname = cx(`${ns}Portlet`, className, {
['no-header']: hideHeader
});
const styleVar =
typeof style === 'string'
? resolveVariable(style, data) || {}
: mapValues(style, s => resolveVariable(s, data) || s);
return (
<div className={portletClassname} style={styleVar}>
{this.renderTabs()}
</div>
);
}
} }
@Renderer({ @Renderer({
type: 'portlet' type: 'portlet'
}) })
export class PortletRenderer extends Portlet { export class PortletRenderer extends Portlet {}
}