Merge remote-tracking branch 'baidu/master'

This commit is contained in:
2betop 2020-04-20 17:31:39 +08:00
commit de2689ba6b
6 changed files with 337 additions and 156 deletions

View File

@ -5,7 +5,7 @@
目前在百度大量用于内部平台的前端开发,已有 100+ 部门使用,创建了 2.5w+ 页面。 目前在百度大量用于内部平台的前端开发,已有 100+ 部门使用,创建了 2.5w+ 页面。
通过 amis 搭建自己的后台系统,可以参考这: https://github.com/fex-team/amis-admin 通过 amis 搭建自己的后台系统,可以参考这: https://github.com/fex-team/amis-admin
包含fis3版本、webpack版本和 jssdk 版本。 包含fis3 版本、webpack 版本和 jssdk 版本。
## 快速开始 ## 快速开始
@ -13,6 +13,8 @@
## 开发指南 ## 开发指南
> 如果 github 下载慢可以使用 [gitee](https://gitee.com/baidu/amis) 上的镜像。
推荐使用 node 10node 6 和 node 8 也可以。node 12 未测试,可能 fis3 还有些插件不支持。 推荐使用 node 10node 6 和 node 8 也可以。node 12 未测试,可能 fis3 还有些插件不支持。
```bash ```bash
@ -46,9 +48,9 @@ npm run coverage
## 维护者 ## 维护者
- [2betop](https://github.com/2betop) - [2betop](https://github.com/2betop)
- [RickCole21](https://github.com/RickCole21) - [RickCole21](https://github.com/RickCole21)
- [catchonme](https://github.com/catchonme) - [catchonme](https://github.com/catchonme)
## 讨论 ## 讨论

View File

@ -1,19 +1,20 @@
### NestedSelect ### NestedSelect
树形结构选择框。 嵌套选择框。
- `type` 请设置成 `nested-select` - `type` 请设置成 `nested-select`
- `options` 类似于 [select](./Select.md) 中 `options`, 并且支持通过 `children` 无限嵌套。 - `options` 类似于 [select](./Select.md) 中 `options`, 并且支持通过 `children` 无限嵌套。
- `source` Api 地址,如果选项不固定,可以通过配置 `source` 动态拉取。 - `source` Api 地址,如果选项不固定,可以通过配置 `source` 动态拉取。
- `multiple` 默认为 `false`, 设置成 `true` 表示可多选。 - `multiple` 默认为 `false`, 设置成 `true` 表示可多选。
- `joinValues` 默认为 `true` - `searchable` 默认为 `false`, 表示是否可搜索
- 单选模式:当用户选中某个选项时,选项中的 value 将被作为该表单项的值提交,否则,整个选项对象都会作为该表单项的值提交。 - `joinValues` 默认为 `true`
- 多选模式:选中的多个选项的 `value` 会通过 `delimiter` 连接起来,否则直接将以数组的形式提交值。 - 单选模式:当用户选中某个选项时,选项中的 value 将被作为该表单项的值提交,否则,整个选项对象都会作为该表单项的值提交。
- `extractValue` 默认为 `false`, `joinValues`设置为`false`时生效, 开启后将选中的选项 value 的值封装为数组,作为当前表单项的值。 - 多选模式:选中的多个选项的 `value` 会通过 `delimiter` 连接起来,否则直接将以数组的形式提交值。
- `delimiter` 默认为 `,` - `extractValue` 默认为 `false`, `joinValues`设置为`false`时生效, 开启后将选中的选项 value 的值封装为数组,作为当前表单项的值。
- `autoFill` 将当前已选中的选项的某个字段的值自动填充到表单中某个表单项中,只在单选时有效 - `delimiter` 默认为 `,`
- `autoFill`的格式为`{address: "${label}"}`,表示将选中项中的`label`的值,自动填充到当前表单项中`name` 为`address` 中 - `cascade` 设置成 `true` 时当选中父节点时不自动选择子节点。
- **还有更多通用配置请参考** [FormItem](./FormItem.md) - `withChildren` 是指成 `true`,选中父节点时,值里面将包含子节点的值,否则只会保留父节点的值。
- **还有更多通用配置请参考** [FormItem](./FormItem.md)
```schema:height="300" scope="form-item" ```schema:height="300" scope="form-item"
{ {

View File

@ -52,6 +52,12 @@
flex-grow: 1; flex-grow: 1;
} }
.#{$ns}Select-arrow {
position: absolute;
right: 0;
top: px2rem(10px);
}
&-clear { &-clear {
padding: px2rem(3px); padding: px2rem(3px);
cursor: pointer; cursor: pointer;
@ -84,15 +90,25 @@
} }
} }
&-menuOuter, &-menuOuter {
&-childrenOuter { display: flex;
z-index: 10; }
position: absolute;
&-menu {
width: 160px;
max-width: 160px;
max-height: px2rem(300px);
background: $Form-select-menu-bg; background: $Form-select-menu-bg;
color: $Form-select-menu-color; color: $Form-select-menu-color;
border: $Form-select-outer-borderWidth solid border: $Form-select-outer-borderWidth solid
$Form-input-onFocused-borderColor; $Form-input-onFocused-borderColor;
box-shadow: $Form-select-outer-boxShadow; box-shadow: $Form-select-outer-boxShadow;
overflow-y: auto;
overflow-x: hidden;
&:not(:first-child) {
border-left: 0;
}
.#{$ns}NestedSelect-option { .#{$ns}NestedSelect-option {
position: relative; position: relative;
@ -100,6 +116,12 @@
height: $Form-input-height; height: $Form-input-height;
line-height: $Form-input-height; line-height: $Form-input-height;
cursor: pointer; cursor: pointer;
display: flex;
> label {
flex: 1;
cursor: pointer;
}
&.is-active { &.is-active {
color: $Form-select-menu-onActive-color; color: $Form-select-menu-onActive-color;
@ -125,14 +147,4 @@
} }
} }
} }
&-childrenOuter {
display: none;
position: relative;
width: 100%;
left: 100%;
transform: translateY(-$Form-input-height);
padding: 0;
margin: 0;
}
} }

View File

@ -6,10 +6,12 @@ import Checkbox from '../../components/Checkbox';
import PopOver from '../../components/PopOver'; import PopOver from '../../components/PopOver';
import {RootCloseWrapper} from 'react-overlays'; import {RootCloseWrapper} from 'react-overlays';
import {Icon} from '../../components/icons'; import {Icon} from '../../components/icons';
import {autobind, flattenTree, isEmpty} from '../../utils/helper'; import {autobind, flattenTree, isEmpty, filterTree} from '../../utils/helper';
import {dataMapping} from '../../utils/tpl-builtin'; import {dataMapping} from '../../utils/tpl-builtin';
import {OptionsControl, OptionsControlProps} from '../Form/Options';
import {OptionsControl, OptionsControlProps, Option} from '../Form/Options'; import {Option, Options} from '../../components/Select';
import Input from '../../components/Input';
import {findDOMNode} from 'react-dom';
export interface NestedSelectProps extends OptionsControlProps { export interface NestedSelectProps extends OptionsControlProps {
cascade?: boolean; cascade?: boolean;
@ -18,6 +20,9 @@ export interface NestedSelectProps extends OptionsControlProps {
export interface NestedSelectState { export interface NestedSelectState {
isOpened?: boolean; isOpened?: boolean;
isFocused?: boolean;
inputValue?: string;
stack: Array<Array<Option>>;
} }
export default class NestedSelectControl extends React.Component< export default class NestedSelectControl extends React.Component<
@ -26,12 +31,16 @@ export default class NestedSelectControl extends React.Component<
> { > {
static defaultProps: Partial<NestedSelectProps> = { static defaultProps: Partial<NestedSelectProps> = {
cascade: false, cascade: false,
withChildren: false withChildren: false,
searchPromptText: '输入内容进行检索'
}; };
target: any; target: any;
alteredOptions: any; input: HTMLInputElement;
state = { state: NestedSelectState = {
isOpened: false isOpened: false,
isFocused: false,
inputValue: '',
stack: []
}; };
@autobind @autobind
@ -41,9 +50,11 @@ export default class NestedSelectControl extends React.Component<
@autobind @autobind
open() { open() {
if (!this.props.disabled) { const {options, disabled} = this.props;
if (!disabled) {
this.setState({ this.setState({
isOpened: true isOpened: true,
stack: [options]
}); });
} }
} }
@ -51,7 +62,8 @@ export default class NestedSelectControl extends React.Component<
@autobind @autobind
close() { close() {
this.setState({ this.setState({
isOpened: false isOpened: false,
stack: []
}); });
} }
@ -101,6 +113,10 @@ export default class NestedSelectControl extends React.Component<
onBulkChange onBulkChange
} = this.props; } = this.props;
if (multiple) {
return;
}
e.stopPropagation(); e.stopPropagation();
const sendTo = const sendTo =
@ -120,7 +136,7 @@ export default class NestedSelectControl extends React.Component<
!multiple && this.close(); !multiple && this.close();
} }
handleCheck(option: any | Array<any>) { handleCheck(option: Option | Options, index?: number) {
const { const {
onChange, onChange,
selectedOptions, selectedOptions,
@ -129,11 +145,33 @@ export default class NestedSelectControl extends React.Component<
delimiter, delimiter,
extractValue, extractValue,
withChildren, withChildren,
cascade cascade,
multiple
} = this.props; } = this.props;
const {stack} = this.state;
if (
!Array.isArray(option) &&
option.children &&
option.children.length &&
typeof index === 'number'
) {
const checked = selectedOptions.some(
o => o.value == (option as Option).value
);
const uncheckable = cascade
? false
: option.uncheckable || (multiple && !checked);
const children = option.children.map(c => ({...c, uncheckable}));
if (stack[index]) {
stack.splice(index + 1, 1, children);
} else {
stack.push(children);
}
}
const items = selectedOptions.concat(); const items = selectedOptions.concat();
let newValue; let newValue: Option | Options | string;
// 三种情况: // 三种情况:
// 1.全选option为数组 // 1.全选option为数组
@ -148,46 +186,56 @@ export default class NestedSelectControl extends React.Component<
newValue = xorBy(items, [option], valueField || 'value'); newValue = xorBy(items, [option], valueField || 'value');
} else if (withChildren) { } else if (withChildren) {
option = flattenTree([option]); option = flattenTree([option]);
const fn = option.every((opt: any) => !!~items.indexOf(opt)) const fn = option.every(
(opt: Option) => !!~items.findIndex(item => item.value === opt.value)
)
? xorBy ? xorBy
: unionBy; : unionBy;
newValue = fn(items, option, valueField || 'value'); newValue = fn(items, option, valueField || 'value');
} else { } else {
newValue = items.filter(item => !~flattenTree([option]).indexOf(item)); newValue = items.filter(
!~items.indexOf(option) && newValue.push(option); item =>
!~flattenTree([option], i => (i as Option).value).indexOf(
item.value
)
);
!~items.map(item => item.value).indexOf(option.value) &&
newValue.push(option);
} }
} else { } else {
newValue = xorBy(items, [option], valueField || 'value'); newValue = xorBy(items, [option], valueField || 'value');
} }
if (joinValues) { if (joinValues) {
newValue = newValue newValue = (newValue as Options)
.map((item: any) => item[valueField || 'value']) .map(item => item[valueField || 'value'])
.join(delimiter || ','); .join(delimiter || ',');
} else if (extractValue) { } else if (extractValue) {
newValue = newValue.map((item: any) => item[valueField || 'value']); newValue = (newValue as Options).map(item => item[valueField || 'value']);
} }
onChange(newValue); onChange(newValue);
} }
allChecked(options: Array<any>): boolean { allChecked(options: Options): boolean {
return options.every((option: any) => { const {selectedOptions, withChildren} = this.props;
if (option.children) { return options.every(option => {
if (withChildren && option.children) {
return this.allChecked(option.children); return this.allChecked(option.children);
} }
return this.props.selectedOptions.some( return selectedOptions.some(
selectedOption => selectedOption.value == option.value selectedOption => selectedOption.value == option.value
); );
}); });
} }
partialChecked(options: Array<any>): boolean { partialChecked(options: Options): boolean {
return options.some((option: any) => { const {selectedOptions, withChildren} = this.props;
if (option.children) { return options.some(option => {
if (withChildren && option.children) {
return this.partialChecked(option.children); return this.partialChecked(option.children);
} }
return this.props.selectedOptions.some( return selectedOptions.some(
selectedOption => selectedOption.value == option.value selectedOption => selectedOption.value == option.value
); );
}); });
@ -198,11 +246,83 @@ export default class NestedSelectControl extends React.Component<
reload && reload(); reload && reload();
} }
renderOptions( @autobind
newOptions: Array<any>, onFocus(e: any) {
isChildren: boolean, this.props.disabled ||
uncheckable: boolean this.state.isOpened ||
): any { this.setState(
{
isFocused: true
},
this.focus
);
this.props.onFocus && this.props.onFocus(e);
}
@autobind
onBlur(e: any) {
this.setState({
isFocused: false
});
this.props.onBlur && this.props.onBlur(e);
}
@autobind
focus() {
this.input
? this.input.focus()
: this.getTarget() && this.getTarget().focus();
}
@autobind
blur() {
this.input
? this.input.blur()
: this.getTarget() && this.getTarget().blur();
}
@autobind
getTarget() {
if (!this.target) {
this.target = findDOMNode(this) as HTMLElement;
}
return this.target as HTMLElement;
}
@autobind
inputRef(ref: HTMLInputElement) {
this.input = ref;
}
@autobind
handleInputChange(evt: React.ChangeEvent<HTMLInputElement>) {
const inputValue = evt.currentTarget.value;
const {options, labelField, valueField} = this.props;
const regexp = new RegExp(`${inputValue}`, 'i');
let filtedOptions =
inputValue && this.state.isOpened
? filterTree(
options,
option =>
regexp.test(option[labelField || 'label']) ||
regexp.test(option[valueField || 'value']) ||
!!(option.children && option.children.length),
1,
true
)
: options.concat();
this.setState({
inputValue,
stack: [filtedOptions]
});
}
renderOptions() {
const { const {
multiple, multiple,
selectedOptions, selectedOptions,
@ -210,87 +330,119 @@ export default class NestedSelectControl extends React.Component<
value, value,
options, options,
disabled, disabled,
cascade searchable,
searchPromptText
} = this.props; } = this.props;
if (multiple) { const stack = this.state.stack;
let partialChecked = this.partialChecked(options);
let allChecked = this.allChecked(options);
return ( const searchInput = searchable ? (
<div className={cx({'NestedSelect-childrenOuter': isChildren})}> <div
{!isChildren ? ( className={cx(`Select-input`, {
<div className={cx('NestedSelect-option', 'checkall')}> 'is-focused': this.state.isFocused
<Checkbox })}
onChange={this.handleCheck.bind(this, options)} >
checked={partialChecked} <Icon icon="search" className="icon" />
partial={partialChecked && !allChecked} <Input
> value={this.state.inputValue}
onFocus={this.onFocus}
</Checkbox> onBlur={this.onBlur}
</div> disabled={disabled}
) : null} placeholder={searchPromptText}
{newOptions.map((option, idx) => { onChange={this.handleInputChange}
const checked = selectedOptions.some(o => o.value == option.value); ref={this.inputRef}
const selfChecked = !!uncheckable || checked; />
let nodeDisabled = !!uncheckable || !!disabled; </div>
) : null;
return ( let partialChecked = this.partialChecked(options);
<div className={cx('NestedSelect-option')} key={idx}> let allChecked = this.allChecked(options);
<Checkbox
onChange={this.handleCheck.bind(this, option)}
trueValue={option.value}
checked={selfChecked}
disabled={nodeDisabled}
>
{option.label}
</Checkbox>
{option.children ? (
<div className={cx('NestedSelect-optionArrowRight')}>
<Icon icon="right-arrow" className="icon" />
</div>
) : null}
{option.children && option.children.length
? this.renderOptions(
option.children,
true,
cascade ? false : uncheckable || (multiple && checked)
)
: null}
</div>
);
})}
</div>
);
}
return ( return (
<div className={cx({'NestedSelect-childrenOuter': isChildren})}> <>
{newOptions.map((option, idx) => ( {stack.map((options, index) => (
<div <div key={index} className={cx('NestedSelect-menu')}>
key={idx} {index === 0 ? searchInput : null}
className={cx('NestedSelect-option', { {multiple && index === 0 ? (
'is-active': value && value === option.value <div className={cx('NestedSelect-option', 'checkall')}>
})} <Checkbox
onClick={this.handleOptionClick.bind(this, option)} onChange={this.handleCheck.bind(this, options)}
> checked={partialChecked}
<span>{option.label}</span> partial={partialChecked && !allChecked}
{option.children ? ( >
<div className={cx('NestedSelect-optionArrowRight')}>
<Icon icon="right-arrow" className="icon" /> </Checkbox>
</div> </div>
) : null} ) : null}
{option.children && option.children.length
? this.renderOptions(option.children, true, false) {options.map((option: Option, idx: number) => {
: null} const checked = selectedOptions.some(
o => o.value == option.value
);
const selfChecked = !!option.uncheckable || checked;
let nodeDisabled = !!option.uncheckable || !!disabled;
return (
<div
key={idx}
className={cx('NestedSelect-option', {
'is-active': value && value === option.value
})}
onClick={this.handleOptionClick.bind(this, option)}
onMouseEnter={this.onMouseEnter.bind(this, option, index)}
>
{multiple ? (
<Checkbox
onChange={this.handleCheck.bind(this, option, index)}
trueValue={option.value}
checked={selfChecked}
disabled={nodeDisabled}
>
{option.label}
</Checkbox>
) : (
<span>{option.label}</span>
)}
{option.children && option.children.length ? (
<div className={cx('NestedSelect-optionArrowRight')}>
<Icon icon="right-arrow" className="icon" />
</div>
) : null}
</div>
);
})}
</div> </div>
))} ))}
</div> </>
); );
} }
onMouseEnter(option: Option, index: number, e: MouseEvent) {
let {stack} = this.state;
let {cascade, multiple, selectedOptions} = this.props;
index = index + 1;
if (option.children && option.children.length) {
const checked = selectedOptions.some(o => o.value == option.value);
const uncheckable = cascade
? false
: option.uncheckable || (multiple && checked);
const children = option.children.map(c => ({...c, uncheckable}));
if (stack[index]) {
stack.splice(index, 1, children);
} else {
stack.push(children);
}
} else {
stack[index] && stack.splice(index, 1);
}
this.setState({stack});
}
renderOuter() { renderOuter() {
const {popOverContainer, options, classnames: cx} = this.props; const {popOverContainer, classnames: cx} = this.props;
let body = ( let body = (
<RootCloseWrapper <RootCloseWrapper
@ -301,24 +453,25 @@ export default class NestedSelectControl extends React.Component<
className={cx('NestedSelect-menuOuter')} className={cx('NestedSelect-menuOuter')}
style={{minWidth: this.target.offsetWidth}} style={{minWidth: this.target.offsetWidth}}
> >
{this.renderOptions(options, false, false)} {this.renderOptions()}
</div> </div>
</RootCloseWrapper> </RootCloseWrapper>
); );
if (popOverContainer) { return (
return ( <Overlay
<Overlay container={popOverContainer} target={() => this.target} show> container={popOverContainer || this.getTarget}
<PopOver target={this.getTarget}
className={cx('NestedSelect-popover')} show
style={{minWidth: this.target.offsetWidth}} >
> <PopOver
{body} className={cx('NestedSelect-popover')}
</PopOver> style={{minWidth: this.target.offsetWidth}}
</Overlay> >
); {body}
} </PopOver>
return body; </Overlay>
);
} }
render() { render() {

View File

@ -66,7 +66,7 @@ export default class Page extends React.Component<PageProps> {
static propsList: Array<string> = [ static propsList: Array<string> = [
'title', 'title',
'subtitle', 'subTitle',
'initApi', 'initApi',
'initFetchOn', 'initFetchOn',
'initFetch', 'initFetch',

View File

@ -833,19 +833,32 @@ export function getTree<T extends TreeItem>(
export function filterTree<T extends TreeItem>( export function filterTree<T extends TreeItem>(
tree: Array<T>, tree: Array<T>,
iterator: (item: T, key: number, level: number) => boolean, iterator: (item: T, key: number, level: number) => boolean,
level: number = 1 level: number = 1,
depthFirst: boolean = false
) { ) {
return tree.filter((item, index) => { if (depthFirst) {
if (!iterator(item, index, level)) { return tree
return false; .map(item => {
} let children: TreeArray | undefined = item.children
? filterTree(item.children, iterator, level + 1, depthFirst)
: undefined;
children && (item = {...item, children: children});
return item;
})
.filter((item, index) => iterator(item, index, level));
}
if (item.children && item.children.splice) { return tree
item.children = filterTree(item.children, iterator, level + 1); .filter((item, index) => iterator(item, index, level))
} .map(item => {
if (item.children && item.children.splice) {
return true; item = {
}); ...item,
children: filterTree(item.children, iterator, level + 1, depthFirst)
};
}
return item;
});
} }
/** /**