diff --git a/docs/zh-CN/components/form/treeselect.md b/docs/zh-CN/components/form/treeselect.md index bc16ea3a2..648103d58 100755 --- a/docs/zh-CN/components/form/treeselect.md +++ b/docs/zh-CN/components/form/treeselect.md @@ -300,6 +300,28 @@ order: 60 } ``` +## 搜索选项 + +> `2.7.1` 及以上版本 + +配置`autoComplete`接口可以实现从远程数据搜索目标结果,搜索的关键字段为`term`,注意搜索的逻辑需要在服务端实现。 + +```schema: scope="body" +{ + "type":"form", + "api":"/api/mock2/form/saveForm", + "body":[ + { + "type":"tree-select", + "name":"tree", + "label":"Tree", + "autoComplete":"/api/mock2/tree/search?term=$term", + "source":"/api/mock2/tree/search" + } + ] +} +``` + ## 属性表 更多用法,见 [InputTree](./input-tree) diff --git a/mock/cfc/mock/tree/search.js b/mock/cfc/mock/tree/search.js new file mode 100644 index 000000000..27eb3df8f --- /dev/null +++ b/mock/cfc/mock/tree/search.js @@ -0,0 +1,183 @@ +/** + * @file tree/autoComplete + * @desc 树形结构的自动搜索 + */ + +const treeOptions = [ + { + label: 'node-0', + value: 'node-0', + children: [ + { + label: 'node-0-0', + value: 'node-0-0', + children: [ + { + label: 'node-0-0-0', + value: 'node-0-0-0', + children: [ + { + label: 'node-0-0-0-0', + value: 'node-0-0-0-0' + }, + { + label: 'node-0-0-0-1', + value: 'node-0-0-0-1' + }, + { + label: 'node-0-0-0-2', + value: 'node-0-0-0-2' + } + ] + }, + { + label: 'node-0-0-1', + value: 'node-0-0-1', + children: [ + { + label: 'node-0-0-1-0', + value: 'node-0-0-1-0' + } + ] + }, + { + label: 'node-0-0-2', + value: 'node-0-0-2', + children: [ + { + label: 'node-0-0-2-0', + value: 'node-0-0-2-0' + }, + { + label: 'node-0-0-2-1', + value: 'node-0-0-2-1' + }, + { + label: 'node-0-0-2-2', + value: 'node-0-0-2-2' + }, + { + label: 'node-0-0-2-3', + value: 'node-0-0-2-3' + } + ] + } + ] + }, + { + label: 'node-0-1', + value: 'node-0-1', + children: [ + { + label: 'node-0-1-0', + value: 'node-0-1-0', + children: [ + { + label: 'node-0-1-0-0', + value: 'node-0-1-0-0' + }, + { + label: 'node-0-1-0-1', + value: 'node-0-1-0-1' + } + ] + }, + { + label: 'node-0-1-1', + value: 'node-0-1-1', + children: [ + { + label: 'node-0-1-1-0', + value: 'node-0-1-1-0' + }, + { + label: 'node-0-1-1-1', + value: 'node-0-1-1-1' + }, + { + label: 'node-0-1-1-2', + value: 'node-0-1-1-2' + } + ] + } + ] + } + ] + }, + { + label: 'node-1', + value: 'node-1', + children: [ + { + label: 'node-1-0', + value: 'node-1-0' + } + ] + }, + { + label: 'node-2', + value: 'node-2' + }, + { + label: 'node-3', + value: 'node-3', + children: [ + { + label: 'node-3-0', + value: 'node-3-0', + children: [ + { + label: 'node-3-0-0', + value: 'node-3-0-0' + }, + { + label: 'node-3-0-1', + value: 'node-3-0-1' + }, + { + label: 'node-3-0-2', + value: 'node-3-0-2' + }, + { + label: 'node-3-0-3', + value: 'node-3-0-3' + } + ] + } + ] + } +]; + +function searchNode(keyword) { + const search = data => { + const matched = []; + + data.forEach(node => { + if (node.value && ~node.value.indexOf(keyword)) { + matched.push({...node}); + } else if (node.children) { + const filtered = search(node.children); + + if (filtered.length) { + matched.push({...node, children: filtered}); + } + } + }); + + return matched; + }; + + return search(treeOptions); +} + +module.exports = function (req, res) { + const term = req.query.term || ''; + + res.json({ + status: 0, + msg: '', + data: { + options: term ? searchNode(term) : treeOptions + } + }); +}; diff --git a/packages/amis/src/renderers/Form/TreeSelect.tsx b/packages/amis/src/renderers/Form/TreeSelect.tsx index 39d15194c..dcc8ea41a 100644 --- a/packages/amis/src/renderers/Form/TreeSelect.tsx +++ b/packages/amis/src/renderers/Form/TreeSelect.tsx @@ -163,6 +163,9 @@ export default class TreeSelectControl extends React.Component< targetRef = (ref: any) => (this.target = ref ? (findDOMNode(ref) as HTMLElement) : null); + /** source数据源是否已加载 */ + sourceLoaded: boolean = false; + constructor(props: TreeSelectProps) { super(props); @@ -183,7 +186,6 @@ export default class TreeSelectControl extends React.Component< leading: false }); this.handleInputKeyDown = this.handleInputKeyDown.bind(this); - this.loadRemote = debouce(this.loadRemote.bind(this), 250, { trailing: true, leading: false @@ -194,6 +196,10 @@ export default class TreeSelectControl extends React.Component< this.loadRemote(''); } + componentWillUnmount() { + this.sourceLoaded = false; + } + open(fn?: () => void) { if (this.props.disabled) { return; @@ -228,7 +234,11 @@ export default class TreeSelectControl extends React.Component< } handleKeyPress(e: React.KeyboardEvent) { - if (e.key === ' ') { + /** + * 考虑到label/value中有空格的case + * 这里使用组合键关闭 win:shift + space,mac:shift + space + */ + if (e.key === ' ' && e.shiftKey) { this.handleOutClick(e as any); e.preventDefault(); } @@ -353,9 +363,15 @@ export default class TreeSelectControl extends React.Component< } async loadRemote(input: string) { - const {autoComplete, env, data, setOptions, setLoading} = this.props; + const {autoComplete, env, data, setOptions, setLoading, source} = + this.props; - if (!isEffectiveApi(autoComplete, data)) { + // 同时配置source和autoComplete时,首次渲染需要加载source数据 + if ( + !isEffectiveApi(autoComplete, data) || + (!input && isEffectiveApi(source) && !this.sourceLoaded) + ) { + this.sourceLoaded = true; return; } else if (!env || !env.fetcher) { throw new Error('fetcher is required');