TreeCheckboxes, ChainedCheckboxes 支持选项延时加载

This commit is contained in:
2betop 2020-05-28 17:47:49 +08:00
parent 8d53af03d1
commit 878bb8c8f3
15 changed files with 346 additions and 104 deletions

View File

@ -386,6 +386,46 @@ export default {
] ]
} }
] ]
},
{
label: '延时加载',
type: 'transfer',
name: 'transfer7',
selectMode: 'tree',
deferApi: '/api/mock2/form/deferOptions?label=${label}',
options: [
{
label: '法师',
children: [
{
label: '诸葛亮',
value: 'zhugeliang'
}
]
},
{
label: '战士',
defer: true
},
{
label: '打野',
children: [
{
label: '李白',
value: 'libai'
},
{
label: '韩信',
value: 'hanxin'
},
{
label: '云中君',
value: 'yunzhongjun'
}
]
}
]
} }
] ]
} }

23
mock/form/deferOptions.js Normal file
View File

@ -0,0 +1,23 @@
module.exports = function (req, res) {
let repeat = 2 + Math.round(Math.random() * 5);
const options = [];
while (repeat--) {
const value = Math.round(Math.random() * 1000000);
const label = value + '';
options.push({
label: label,
value: value,
defer: Math.random() > 0.7
});
}
res.json({
status: 0,
msg: '',
data: {
options: options
}
});
};

View File

@ -404,6 +404,7 @@
&-sublist { &-sublist {
position: relative; position: relative;
margin: 0 0 0 px2rem(35px); margin: 0 0 0 px2rem(35px);
display: none;
&:before { &:before {
width: 1px; width: 1px;
@ -420,8 +421,8 @@
&-item { &-item {
position: relative; position: relative;
&.is-collapsed > .#{$ns}TreeCheckboxes-sublist { &.is-expanded > .#{$ns}TreeCheckboxes-sublist {
display: none; display: block;
} }
} }
@ -442,6 +443,7 @@
&-itemInner { &-itemInner {
display: flex; display: flex;
align-items: center;
height: $Form-input-height; height: $Form-input-height;
line-height: $Form-input-lineHeight; line-height: $Form-input-lineHeight;
font-size: $Form-input-fontSize; font-size: $Form-input-fontSize;
@ -452,6 +454,7 @@
> .#{$ns}Checkbox { > .#{$ns}Checkbox {
margin-right: 0; margin-right: 0;
margin-left: $gap-sm;
} }
cursor: pointer; cursor: pointer;
user-select: none; user-select: none;
@ -481,7 +484,7 @@
&-col { &-col {
flex-grow: 1; flex-grow: 1;
min-width: 120px; min-width: 150px;
} }
&-col:not(:last-child) { &-col:not(:last-child) {

View File

@ -6,9 +6,10 @@ import Checkbox from './Checkbox';
import {Option} from './Select'; import {Option} from './Select';
import {getTreeDepth} from '../utils/helper'; import {getTreeDepth} from '../utils/helper';
import times from 'lodash/times'; import times from 'lodash/times';
import Spinner from './Spinner';
export interface ChainedCheckboxesState { export interface ChainedCheckboxesState {
selected: Array<Option>; selected: Array<string>;
} }
export class ChainedCheckboxes extends Checkboxes< export class ChainedCheckboxes extends Checkboxes<
@ -20,17 +21,22 @@ export class ChainedCheckboxes extends Checkboxes<
selected: [] selected: []
}; };
selectOption(option: Option, depth: number) { selectOption(option: Option, depth: number, id: string) {
const {onDeferLoad} = this.props;
const selected = this.state.selected.concat(); const selected = this.state.selected.concat();
selected.splice(depth, selected.length - depth); selected.splice(depth, selected.length - depth);
selected.push(option); selected.push(id);
this.setState({ this.setState(
selected {
}); selected
},
option.defer && onDeferLoad ? () => onDeferLoad(option) : undefined
);
} }
renderOption(option: Option, index: number, depth: number) { renderOption(option: Option, index: number, depth: number, id: string) {
const { const {
labelClassName, labelClassName,
disabled, disabled,
@ -40,7 +46,7 @@ export class ChainedCheckboxes extends Checkboxes<
} = this.props; } = this.props;
const valueArray = this.valueArray; const valueArray = this.valueArray;
if (Array.isArray(option.children)) { if (Array.isArray(option.children) || option.defer) {
return ( return (
<div <div
key={index} key={index}
@ -49,13 +55,15 @@ export class ChainedCheckboxes extends Checkboxes<
itemClassName, itemClassName,
option.className, option.className,
disabled || option.disabled ? 'is-disabled' : '', disabled || option.disabled ? 'is-disabled' : '',
~this.state.selected.indexOf(option) ? 'is-active' : '' ~this.state.selected.indexOf(id) ? 'is-active' : ''
)} )}
onClick={() => this.selectOption(option, depth)} onClick={() => this.selectOption(option, depth, id)}
> >
<div className={cx('ChainedCheckboxes-itemLabel')}> <div className={cx('ChainedCheckboxes-itemLabel')}>
{itemRender(option)} {itemRender(option)}
</div> </div>
{option.defer && option.loading ? <Spinner size="sm" show /> : null}
</div> </div>
); );
} }
@ -101,26 +109,29 @@ export class ChainedCheckboxes extends Checkboxes<
let body: Array<React.ReactNode> = []; let body: Array<React.ReactNode> = [];
if (Array.isArray(options) && options.length) { if (Array.isArray(options) && options.length) {
const selected: Array<Option | null> = this.state.selected.concat(); const selected: Array<string | null> = this.state.selected.concat();
const depth = getTreeDepth(options); const depth = Math.min(getTreeDepth(options), 3);
times(depth - selected.length, () => selected.push(null)); times(Math.max(depth - selected.length, 1), () => selected.push(null));
selected.reduce( selected.reduce(
( (
{ {
body, body,
options, options,
subTitle subTitle,
indexes
}: { }: {
body: Array<React.ReactNode>; body: Array<React.ReactNode>;
options: Array<Option> | null; options: Array<Option> | null;
subTitle?: string; subTitle?: string;
indexes: Array<number>;
}, },
selected, selected,
depth depth
) => { ) => {
let nextOptions: Array<Option> = []; let nextOptions: Array<Option> = [];
let nextSubTitle: string = ''; let nextSubTitle: string = '';
let nextIndexes = indexes;
body.push( body.push(
<div key={depth} className={cx('ChainedCheckboxes-col')}> <div key={depth} className={cx('ChainedCheckboxes-col')}>
@ -131,12 +142,15 @@ export class ChainedCheckboxes extends Checkboxes<
) : null} ) : null}
{Array.isArray(options) && options.length {Array.isArray(options) && options.length
? options.map((option, index) => { ? options.map((option, index) => {
if (option === selected) { const id = indexes.concat(index).join('-');
if (id === selected) {
nextSubTitle = option.subTitle; nextSubTitle = option.subTitle;
nextOptions = option.children!; nextOptions = option.children!;
nextIndexes = indexes.concat(index);
} }
return this.renderOption(option, index, depth); return this.renderOption(option, index, depth, id);
}) })
: null} : null}
</div> </div>
@ -145,12 +159,14 @@ export class ChainedCheckboxes extends Checkboxes<
return { return {
options: nextOptions, options: nextOptions,
subTitle: nextSubTitle, subTitle: nextSubTitle,
indexes: nextIndexes,
body: body body: body
}; };
}, },
{ {
options, options,
body body,
indexes: []
} }
); );
} }

View File

@ -21,6 +21,7 @@ export interface CheckboxesProps extends ThemeProps {
placeholder?: string; placeholder?: string;
value?: Array<any>; value?: Array<any>;
onChange?: (value: Array<Option>) => void; onChange?: (value: Array<Option>) => void;
onDeferLoad?: (option: Option) => void;
inline?: boolean; inline?: boolean;
labelClassName?: string; labelClassName?: string;
option2value?: (option: Option) => any; option2value?: (option: Option) => any;

View File

@ -18,6 +18,7 @@ export class ListCheckboxes extends Checkboxes {
} = this.props; } = this.props;
const valueArray = this.valueArray; const valueArray = this.valueArray;
// todo 支持 option.defer 延时加载
if (Array.isArray(option.children)) { if (Array.isArray(option.children)) {
return ( return (
<div <div

View File

@ -22,15 +22,45 @@ import {findDOMNode} from 'react-dom';
import {ClassNamesFn, themeable} from '../theme'; import {ClassNamesFn, themeable} from '../theme';
import Checkbox from './Checkbox'; import Checkbox from './Checkbox';
import Input from './Input'; import Input from './Input';
import {Api} from '../types';
export interface Option { export interface Option {
label?: string; label?: string;
// 可以用来给 Option 标记个范围,让数据展示更清晰。
// 这个只有在数值展示的时候显示。
scopeLabel?: string;
// 请保证数值唯一,多个选项值一致会认为是同一个选项。
value?: any; value?: any;
// 是否禁用
disabled?: boolean; disabled?: boolean;
// 支持嵌套
children?: Options; children?: Options;
// 是否可见
visible?: boolean; visible?: boolean;
// 最好不要用!因为有 visible 就够了。
hidden?: boolean; hidden?: boolean;
// 描述
description?: string; description?: string;
// 标记后数据延时加载
defer?: boolean;
// 如果设置了,优先级更高,不设置走 source 接口加载。
deferApi?: Api;
// 标记正在加载。只有 defer 为 true 时有意义。内部字段不可以外部设置
loading?: boolean;
// 只有设置了 defer 才有意义,内部字段不可以外部设置
loaded?: boolean;
[propName: string]: any; [propName: string]: any;
} }
export interface Options extends Array<Option> {} export interface Options extends Array<Option> {}

View File

@ -85,7 +85,8 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
value, value,
onChange, onChange,
onSearch: searchable, onSearch: searchable,
option2value option2value,
onDeferLoad
} = this.props; } = this.props;
if (!Array.isArray(options) || !options.length) { if (!Array.isArray(options) || !options.length) {
@ -130,6 +131,7 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
value={value} value={value}
onChange={onChange} onChange={onChange}
option2value={option2value} option2value={option2value}
onDeferLoad={onDeferLoad}
/> />
) : option.selectMode === 'tree' ? ( ) : option.selectMode === 'tree' ? (
<TreeCheckboxes <TreeCheckboxes
@ -138,6 +140,7 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
value={value} value={value}
onChange={onChange} onChange={onChange}
option2value={option2value} option2value={option2value}
onDeferLoad={onDeferLoad}
/> />
) : option.selectMode === 'chained' ? ( ) : option.selectMode === 'chained' ? (
<ChainedCheckboxes <ChainedCheckboxes
@ -146,6 +149,7 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
value={value} value={value}
onChange={onChange} onChange={onChange}
option2value={option2value} option2value={option2value}
onDeferLoad={onDeferLoad}
/> />
) : ( ) : (
<ListCheckboxes <ListCheckboxes
@ -154,6 +158,7 @@ export class TabsTransfer extends React.Component<TabsTransferProps> {
value={value} value={value}
onChange={onChange} onChange={onChange}
option2value={option2value} option2value={option2value}
onDeferLoad={onDeferLoad}
/> />
)} )}
</Tab> </Tab>

View File

@ -299,7 +299,8 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
value, value,
onChange, onChange,
option2value, option2value,
classnames: cx classnames: cx,
onDeferLoad
} = this.props; } = this.props;
return selectMode === 'table' ? ( return selectMode === 'table' ? (
@ -310,6 +311,7 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
value={value} value={value}
onChange={onChange} onChange={onChange}
option2value={option2value} option2value={option2value}
onDeferLoad={onDeferLoad}
/> />
) : selectMode === 'tree' ? ( ) : selectMode === 'tree' ? (
<TreeCheckboxes <TreeCheckboxes
@ -318,6 +320,7 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
value={value} value={value}
onChange={onChange} onChange={onChange}
option2value={option2value} option2value={option2value}
onDeferLoad={onDeferLoad}
/> />
) : selectMode === 'chained' ? ( ) : selectMode === 'chained' ? (
<ChainedCheckboxes <ChainedCheckboxes
@ -326,6 +329,7 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
value={value} value={value}
onChange={onChange} onChange={onChange}
option2value={option2value} option2value={option2value}
onDeferLoad={onDeferLoad}
/> />
) : ( ) : (
<ListCheckboxes <ListCheckboxes
@ -334,6 +338,7 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
value={value} value={value}
onChange={onChange} onChange={onChange}
option2value={option2value} option2value={option2value}
onDeferLoad={onDeferLoad}
/> />
); );
} }
@ -368,7 +373,7 @@ export class Transfer extends React.Component<TransferProps, TransferState> {
> >
<div className={cx('Transfer-select')}>{this.renderSelect()}</div> <div className={cx('Transfer-select')}>{this.renderSelect()}</div>
<div className={cx('Transfer-mid')}> <div className={cx('Transfer-mid')}>
{showArrow ? ( {showArrow /*todo 需要改成确认模式,即:点了按钮才到右边 */ ? (
<div className={cx('Transfer-arrow')}> <div className={cx('Transfer-arrow')}>
<Icon icon="right-arrow" /> <Icon icon="right-arrow" />
</div> </div>

View File

@ -4,14 +4,15 @@ import React from 'react';
import uncontrollable from 'uncontrollable'; import uncontrollable from 'uncontrollable';
import Checkbox from './Checkbox'; import Checkbox from './Checkbox';
import {Option} from './Select'; import {Option} from './Select';
import {autobind, eachTree} from '../utils/helper'; import {autobind, eachTree, everyTree} from '../utils/helper';
import Spinner from './Spinner';
export interface TreeCheckboxesProps extends CheckboxesProps { export interface TreeCheckboxesProps extends CheckboxesProps {
expand?: 'all' | 'first' | 'root' | 'none'; expand?: 'all' | 'first' | 'root' | 'none';
} }
export interface TreeCheckboxesState { export interface TreeCheckboxesState {
expanded: Array<Option>; expanded: Array<string>;
} }
export class TreeCheckboxes extends Checkboxes< export class TreeCheckboxes extends Checkboxes<
@ -25,7 +26,7 @@ export class TreeCheckboxes extends Checkboxes<
static defaultProps = { static defaultProps = {
...Checkboxes.defaultProps, ...Checkboxes.defaultProps,
expand: 'first' expand: 'first' as 'first'
}; };
componentDidMount() { componentDidMount() {
@ -46,25 +47,26 @@ export class TreeCheckboxes extends Checkboxes<
syncExpanded() { syncExpanded() {
const options = this.props.options; const options = this.props.options;
const mode = this.props.expand; const mode = this.props.expand;
const expanded: Array<Option> = []; const expanded: Array<string> = [];
if (!Array.isArray(options)) { if (!Array.isArray(options)) {
return; return;
} }
if (mode === 'first' || mode === 'root') { if (mode === 'first' || mode === 'root') {
options.every(option => { options.every((option, index) => {
if (Array.isArray(option.children)) { if (Array.isArray(option.children)) {
expanded.push(option); expanded.push(`${index}`);
return mode === 'root'; return mode === 'root';
} }
return true; return true;
}); });
} else if (mode === 'all') { } else if (mode === 'all') {
eachTree(options, option => { everyTree(options, (option, index, level, paths, indexes) => {
if (Array.isArray(option.children)) { if (Array.isArray(option.children)) {
expanded.push(option); expanded.push(`${indexes.concat(index).join('-')}`);
} }
return true;
}); });
} }
@ -72,7 +74,7 @@ export class TreeCheckboxes extends Checkboxes<
} }
toggleOption(option: Option) { toggleOption(option: Option) {
const {value, onChange, option2value, options} = this.props; const {value, onChange, option2value, options, onDeferLoad} = this.props;
if (option.disabled) { if (option.disabled) {
return; return;
@ -124,22 +126,26 @@ export class TreeCheckboxes extends Checkboxes<
onChange && onChange(newValue); onChange && onChange(newValue);
} }
toggleCollapsed(option: Option) { toggleCollapsed(option: Option, index: string) {
const onDeferLoad = this.props.onDeferLoad;
const expanded = this.state.expanded.concat(); const expanded = this.state.expanded.concat();
const idx = expanded.indexOf(option); const idx = expanded.indexOf(index);
if (~idx) { if (~idx) {
expanded.splice(idx, 1); expanded.splice(idx, 1);
} else { } else {
expanded.push(option); expanded.push(index);
} }
this.setState({ this.setState(
expanded: expanded {
}); expanded: expanded
},
option.defer && onDeferLoad ? () => onDeferLoad(option) : undefined
);
} }
renderItem(option: Option, index: number) { renderItem(option: Option, index: number, indexes: Array<number> = []) {
const { const {
labelClassName, labelClassName,
disabled, disabled,
@ -147,6 +153,7 @@ export class TreeCheckboxes extends Checkboxes<
itemClassName, itemClassName,
itemRender itemRender
} = this.props; } = this.props;
const id = indexes.join('-');
const valueArray = this.valueArray; const valueArray = this.valueArray;
let partial = false; let partial = false;
let checked = false; let checked = false;
@ -179,15 +186,16 @@ export class TreeCheckboxes extends Checkboxes<
checked = !!~valueArray.indexOf(option); checked = !!~valueArray.indexOf(option);
} }
const expaned = !!~this.state.expanded.indexOf(option); const expaned = !!~this.state.expanded.indexOf(id);
// todo 支持 option.defer 延时加载
return ( return (
<div <div
key={index} key={index}
className={cx( className={cx(
'TreeCheckboxes-item', 'TreeCheckboxes-item',
disabled || option.disabled ? 'is-disabled' : '', disabled || option.disabled || (option.defer && option.loading)
? 'is-disabled'
: '',
expaned ? 'is-expanded' : '' expaned ? 'is-expanded' : ''
)} )}
> >
@ -199,11 +207,11 @@ export class TreeCheckboxes extends Checkboxes<
)} )}
onClick={() => this.toggleOption(option)} onClick={() => this.toggleOption(option)}
> >
{hasChildren ? ( {hasChildren || option.defer ? (
<a <a
onClick={(e: React.MouseEvent<any>) => { onClick={(e: React.MouseEvent<any>) => {
e.stopPropagation(); e.stopPropagation();
this.toggleCollapsed(option); this.toggleCollapsed(option, id);
}} }}
className={cx('Table-expandBtn', expaned ? 'is-active' : '')} className={cx('Table-expandBtn', expaned ? 'is-active' : '')}
> >
@ -215,19 +223,23 @@ export class TreeCheckboxes extends Checkboxes<
{itemRender(option)} {itemRender(option)}
</div> </div>
<Checkbox {option.defer && option.loading ? <Spinner show size="sm" /> : null}
size="sm"
checked={checked} {!option.defer || option.loaded ? (
partial={partial} <Checkbox
disabled={disabled || option.disabled} size="sm"
labelClassName={labelClassName} checked={checked}
description={option.description} partial={partial}
/> disabled={disabled || option.disabled}
labelClassName={labelClassName}
description={option.description}
/>
) : null}
</div> </div>
{hasChildren ? ( {hasChildren ? (
<div className={cx('TreeCheckboxes-sublist')}> <div className={cx('TreeCheckboxes-sublist')}>
{option.children!.map((option, key) => {option.children!.map((option, key) =>
this.renderItem(option, key) this.renderItem(option, key, indexes.concat(key))
)} )}
</div> </div>
) : null} ) : null}
@ -249,7 +261,7 @@ export class TreeCheckboxes extends Checkboxes<
let body: Array<React.ReactNode> = []; let body: Array<React.ReactNode> = [];
if (Array.isArray(options) && options.length) { if (Array.isArray(options) && options.length) {
body = options.map((option, key) => this.renderItem(option, key)); body = options.map((option, key) => this.renderItem(option, key, [key]));
} }
return ( return (

View File

@ -7,11 +7,7 @@ import Downshift, {StateChangeOptions} from 'downshift';
import {autobind} from '../../utils/helper'; import {autobind} from '../../utils/helper';
import {ICONS} from './IconPickerIcons'; import {ICONS} from './IconPickerIcons';
import {FormItem, FormControlProps} from './Item'; import {FormItem, FormControlProps} from './Item';
import {Option} from '../../components/Select';
export interface Option {
label?: string;
value?: any;
}
export interface IconPickerProps extends FormControlProps { export interface IconPickerProps extends FormControlProps {
placeholder?: string; placeholder?: string;

View File

@ -62,6 +62,7 @@ export interface OptionsControlProps extends FormControlProps, OptionProps {
setOptions: (value: Array<any>, skipNormalize?: boolean) => void; setOptions: (value: Array<any>, skipNormalize?: boolean) => void;
setLoading: (value: boolean) => void; setLoading: (value: boolean) => void;
reloadOptions: () => void; reloadOptions: () => void;
deferLoad: (option: Option) => void;
creatable?: boolean; creatable?: boolean;
onAdd?: ( onAdd?: (
idx?: number | Array<number>, idx?: number | Array<number>,
@ -79,6 +80,7 @@ export interface OptionsControlProps extends FormControlProps, OptionProps {
// 自己接收的属性。 // 自己接收的属性。
export interface OptionsProps extends FormControlProps, OptionProps { export interface OptionsProps extends FormControlProps, OptionProps {
source?: Api; source?: Api;
deferApi?: Api;
creatable?: boolean; creatable?: boolean;
addApi?: Api; addApi?: Api;
addControls?: Array<any>; addControls?: Array<any>;
@ -449,6 +451,27 @@ export function registerOptionsControl(config: OptionsConfig) {
return formItem.loadOptions(source, data, undefined, false, onChange); return formItem.loadOptions(source, data, undefined, false, onChange);
} }
@autobind
deferLoad(option: Option) {
const {deferApi, source, env, formItem, data} = this.props;
if (option.loaded) {
return;
}
const api = option.deferApi || deferApi || source;
if (!api) {
env.notify(
'error',
'请在选项中设置 `deferApi` 或者表单项中设置 `deferApi`,用来加载子选项。'
);
return;
}
formItem?.deferLoadOptions(option, api, createObject(data, option));
}
@autobind @autobind
async initOptions(data: any) { async initOptions(data: any) {
await this.reload(); await this.reload();
@ -777,6 +800,7 @@ export function registerOptionsControl(config: OptionsConfig) {
setOptions={this.setOptions} setOptions={this.setOptions}
syncOptions={this.syncOptions} syncOptions={this.syncOptions}
reloadOptions={this.reload} reloadOptions={this.reload}
deferLoad={this.deferLoad}
creatable={ creatable={
creatable || (creatable !== false && isEffectiveApi(addApi)) creatable || (creatable !== false && isEffectiveApi(addApi))
} }

View File

@ -149,7 +149,8 @@ export class TransferRenderer<
columns, columns,
loading, loading,
searchable, searchable,
searchResultMode searchResultMode,
deferLoad
} = this.props; } = this.props;
return ( return (
@ -165,6 +166,7 @@ export class TransferRenderer<
searchResultMode={searchResultMode} searchResultMode={searchResultMode}
columns={columns} columns={columns}
onSearch={searchable ? this.handleSearch : undefined} onSearch={searchable ? this.handleSearch : undefined}
onDeferLoad={deferLoad}
/> />
<Spinner overlay key="info" show={loading} /> <Spinner overlay key="info" show={loading} />

View File

@ -17,7 +17,9 @@ import {
isObject, isObject,
createObject, createObject,
isObjectShallowModified, isObjectShallowModified,
findTree findTree,
findTreeIndex,
spliceTree
} from '../utils/helper'; } from '../utils/helper';
import {flattenTree} from '../utils/helper'; import {flattenTree} from '../utils/helper';
import {IRendererStore} from '.'; import {IRendererStore} from '.';
@ -170,13 +172,13 @@ export const FormItemStore = types
unMatched = { unMatched = {
[self.valueField || 'value']: item, [self.valueField || 'value']: item,
[self.labelField || 'label']: item, [self.labelField || 'label']: item,
'__unmatched': true __unmatched: true
}; };
} else if (unMatched && self.extractValue) { } else if (unMatched && self.extractValue) {
unMatched = { unMatched = {
[self.valueField || 'value']: item, [self.valueField || 'value']: item,
[self.labelField || 'label']: 'UnKnown', [self.labelField || 'label']: 'UnKnown',
'__unmatched': true __unmatched: true
}; };
} }
@ -349,22 +351,14 @@ export const FormItemStore = types
} }
let loadCancel: Function | null = null; let loadCancel: Function | null = null;
const loadOptions: ( const fetchOptions: (
api: Api, api: Api,
data?: object, data?: object,
options?: fetchOptions, config?: fetchOptions
clearValue?: boolean,
onChange?: (value: any) => void
) => Promise<Payload | null> = flow(function* getInitData( ) => Promise<Payload | null> = flow(function* getInitData(
api: string, api: string,
data: object, data: object,
options?: fetchOptions, config?: fetchOptions
clearValue?: any,
onChange?: (
value: any,
submitOnChange: boolean,
changeImmediately: boolean
) => void
) { ) {
try { try {
if (loadCancel) { if (loadCancel) {
@ -381,15 +375,15 @@ export const FormItemStore = types
{ {
autoAppend: false, autoAppend: false,
cancelExecutor: (executor: Function) => (loadCancel = executor), cancelExecutor: (executor: Function) => (loadCancel = executor),
...options ...config
} }
); );
loadCancel = null; loadCancel = null;
let result: any = null;
if (!json.ok) { if (!json.ok) {
setError( setError(
`加载选项失败,原因:${json.msg || `加载选项失败,原因:${json.msg || (config && config.errorMessage)}`
(options && options.errorMessage)}`
); );
(getRoot(self) as IRendererStore).notify( (getRoot(self) as IRendererStore).notify(
'error', 'error',
@ -402,29 +396,11 @@ export const FormItemStore = types
: undefined : undefined
); );
} else { } else {
clearError(); result = json;
self.validated = false; // 拉完数据应该需要再校验一下
let options: Array<IOption> =
json.data?.options ||
json.data.items ||
json.data.rows ||
json.data ||
[];
options = normalizeOptions(options as any);
setOptions(options);
if (json.data && typeof (json.data as any).value !== 'undefined') {
onChange && onChange((json.data as any).value, false, true);
} else if (clearValue) {
self.selectedOptions.some((item: any) => item.__unmatched) &&
onChange &&
onChange('', false, true);
}
} }
self.loading = false; self.loading = false;
return json; return result;
} catch (e) { } catch (e) {
const root = getRoot(self) as IRendererStore; const root = getRoot(self) as IRendererStore;
if (root.storeType !== 'RendererStore') { if (root.storeType !== 'RendererStore') {
@ -441,10 +417,103 @@ export const FormItemStore = types
console.error(e.stack); console.error(e.stack);
getRoot(self) && getRoot(self) &&
(getRoot(self) as IRendererStore).notify('error', e.message); (getRoot(self) as IRendererStore).notify('error', e.message);
return null; return;
} }
} as any); } as any);
const loadOptions: (
api: Api,
data?: object,
config?: fetchOptions,
clearValue?: boolean,
onChange?: (value: any) => void
) => Promise<Payload | null> = flow(function* getInitData(
api: string,
data: object,
config?: fetchOptions,
clearValue?: any,
onChange?: (
value: any,
submitOnChange: boolean,
changeImmediately: boolean
) => void
) {
let json = yield fetchOptions(api, data, config);
if (!json) {
return;
}
clearError();
self.validated = false; // 拉完数据应该需要再校验一下
let options: Array<IOption> =
json.data?.options ||
json.data.items ||
json.data.rows ||
json.data ||
[];
options = normalizeOptions(options as any);
setOptions(options);
if (json.data && typeof (json.data as any).value !== 'undefined') {
onChange && onChange((json.data as any).value, false, true);
} else if (clearValue) {
self.selectedOptions.some((item: any) => item.__unmatched) &&
onChange &&
onChange('', false, true);
}
return json;
});
const deferLoadOptions: (
option: any,
api: Api,
data?: object,
config?: fetchOptions
) => Promise<Payload | null> = flow(function* getInitData(
option: any,
api: string,
data: object,
config?: fetchOptions
) {
const indexes = findTreeIndex(self.options, item => item === option);
if (!indexes) {
return;
}
setOptions(
spliceTree(self.options, indexes, 1, {
...option,
loading: true
})
);
let json = yield fetchOptions(api, data, config);
if (!json) {
return;
}
let options: Array<IOption> =
json.data?.options ||
json.data.items ||
json.data.rows ||
json.data ||
[];
setOptions(
spliceTree(self.options, indexes, 1, {
...option,
loading: false,
loaded: true,
children: options
})
);
return json;
});
function syncOptions(originOptions?: Array<any>) { function syncOptions(originOptions?: Array<any>) {
if (!self.options.length && typeof self.value === 'undefined') { if (!self.options.length && typeof self.value === 'undefined') {
self.selectedOptions = []; self.selectedOptions = [];
@ -527,7 +596,7 @@ export const FormItemStore = types
unMatched = { unMatched = {
[self.valueField || 'value']: item, [self.valueField || 'value']: item,
[self.labelField || 'label']: item, [self.labelField || 'label']: item,
'__unmatched': true __unmatched: true
}; };
const orgin: any = const orgin: any =
@ -545,7 +614,7 @@ export const FormItemStore = types
unMatched = { unMatched = {
[self.valueField || 'value']: item, [self.valueField || 'value']: item,
[self.labelField || 'label']: 'UnKnown', [self.labelField || 'label']: 'UnKnown',
'__unmatched': true __unmatched: true
}; };
} }
@ -631,6 +700,7 @@ export const FormItemStore = types
clearError, clearError,
setOptions, setOptions,
loadOptions, loadOptions,
deferLoadOptions,
syncOptions, syncOptions,
setLoading, setLoading,
setSubStore, setSubStore,

View File

@ -892,15 +892,28 @@ export function filterTree<T extends TreeItem>(
*/ */
export function everyTree<T extends TreeItem>( export function everyTree<T extends TreeItem>(
tree: Array<T>, tree: Array<T>,
iterator: (item: T, key: number, level: number, paths: Array<T>) => boolean, iterator: (
item: T,
key: number,
level: number,
paths: Array<T>,
indexes: Array<number>
) => boolean,
level: number = 1, level: number = 1,
paths: Array<T> = [] paths: Array<T> = [],
indexes: Array<number> = []
): boolean { ): boolean {
return tree.every((item, index) => { return tree.every((item, index) => {
const value: any = iterator(item, index, level, paths); const value: any = iterator(item, index, level, paths, indexes);
if (value && item.children && item.children.splice) { if (value && item.children && item.children.splice) {
return everyTree(item.children, iterator, level + 1, paths.concat(item)); return everyTree(
item.children,
iterator,
level + 1,
paths.concat(item),
indexes.concat(index)
);
} }
return value; return value;
@ -975,6 +988,7 @@ export function spliceTree<T extends TreeItem>(
if (typeof idx === 'number') { if (typeof idx === 'number') {
list.splice(idx, deleteCount, ...items); list.splice(idx, deleteCount, ...items);
} else if (Array.isArray(idx) && idx.length) { } else if (Array.isArray(idx) && idx.length) {
idx = idx.concat();
const lastIdx = idx.pop()!; const lastIdx = idx.pop()!;
let host = idx.reduce((list: Array<T>, idx) => { let host = idx.reduce((list: Array<T>, idx) => {
const child = { const child = {