2026/9/13 19:23:56

amis TreeSelect 树形选择器完整指南:从基本使用到事件动作与源码剖析

amis TreeSelect 树形选择器完整指南:从基本使用到事件动作与源码剖析 amis TreeSelect 树形选择器完整指南从基本使用到事件动作与源码剖析【免费下载链接】amis前端低代码框架通过 JSON 配置就能生成各种页面。项目地址: https://gitcode.com/GitHub_Trending/am/amisTreeSelect树形选择器是 amis 前端低代码框架中用于在表单中以树形结构选择数据的核心组件适合组织架构、目录文件、分类层级等场景。本文基于 treeselect.md 文档完整覆盖其基本用法、路径文本隐藏、叶子节点限制、远程搜索、自定义渲染、事件与动作体系并结合 TreeSelect.tsx 源码与 Tree.test.tsx 测试用例深入讲解底层实现帮助你在表单中快速落地树形选择能力。基本使用TreeSelect 通过在表单项 schema 中设置type: tree-select启用数据通过options配置借助children字段实现多层级的树形嵌套。其完整形态是一个可展开的下拉浮层左侧展示树形节点选中后结果回填到输入框中。{ type: form, api: /api/mock2/form/saveForm, body: [ { type: tree-select, name: tree, label: Tree, searchable: true, options: [ { label: Folder A, value: 1, children: [ { label: file A, value: 2 }, { label: file B, value: 3 } ] }, { label: file C, value: 4 }, { label: file D, value: 5 }, { label: Folder E, children: [ { label: Folder G, children: [ { label: file H, value: 6 }, { label: file I, value: 7 } ] } ] } ] } ] }从上例可以看到树形数据的关键约定每个节点通过label显示文本与value选中值描述字段名可用labelField、valueField自定义children数组表示子节点可无限嵌套形成多级树节点可以只有label而没有value如Folder E这样的节点不可被选中详见下文如何让某些节点无法点。从源码看TreeSelect.tsx 中TreeSelectControl的defaultProps给出了组件默认行为hideRoot: true隐藏顶级节点、multiple: false默认单选、clearable: true可清除、joinValues: true且delimiter: ,多选时以逗号拼接提交值、resetValue: 、hideNodePathLabel: false、pathSeparator: /等。这意味着你无需显式配置这些项即可获得开箱即用的体验。仅展示选中节点文本信息多选模式下默认选择框内会展示选中节点 其所有祖先节点的完整路径文本祖先与当前节点之间以/拼接。如果只想展示当前选中节点自身的文本设置hideNodePathLabel: true即可隐藏祖先节点的labelField值。{ type: form, api: /api/mock2/form/saveForm, body: [ { type: tree-select, name: tree1, label: 展示已选择节点的祖先节点的文本信息, value: 1,6,7, multiple: true, options: [ { label: Folder A, value: 1, children: [ { label: file A, value: 2 }, { label: file B, value: 3 } ] }, { label: file C, value: 4 }, { label: file D, value: 5 }, { label: Folder E, children: [ { label: Folder G, children: [ { label: file H, value: 6 }, { label: file I, value: 7 } ] } ] } ] }, { type: divider }, { type: tree-select, name: tree2, label: 仅展示已选择节点的文本信息, value: 1,6,7, multiple: true, hideNodePathLabel: true, options: [ { label: Folder A, value: 1, children: [ { label: file A, value: 2 }, { label: file B, value: 3 } ] }, { label: file C, value: 4 }, { label: file D, value: 5 }, { label: Folder E, children: [ { label: Folder G, children: [ { label: file H, value: 6 }, { label: file I, value: 7 } ] } ] } ] } ] }其实现原理位于 TreeSelect.tsx 的renderItem方法当hideNodePathLabel为true时直接返回item[labelField || label]即仅当前节点的文本否则通过getTreeAncestors(options, item, true)取到全部祖先节点将所有节点的labelField值用 / 连接成完整路径字符串。也就是说展示在输入框中的路径文本是纯前端基于options树实时计算出来的与后端无关因此即使值来自接口回显也能正确渲染出层级路径。只允许选择叶子节点1.8.0 及以上版本在单选场景下通过onlyLeaf: true可以限制用户只能选择叶子节点即没有children的节点非叶子节点将无法被选中{ type: form, api: /api/mock2/form/saveForm, body: [ { type: tree-select, name: tree, label: Tree, onlyLeaf: true, searchable: true, options: [ { label: Folder A, value: 1, children: [ { label: file A, value: 2 }, { label: file B, value: 3 } ] }, { label: file C, value: 4 }, { label: file D, value: 5 }, { label: Folder E, value: 61, children: [ { label: Folder G, value: 62, children: [ { label: file H, value: 6 }, { label: file I, value: 7 } ] } ] } ] } ] }在源码中onlyLeaf会被透传给底层的 Tree 选择组件TreeSelect.tsx 的renderOuter中onlyLeaf{onlyLeaf}由树组件在节点点击时拦截非叶子节点的选择行为。如何让某些节点无法点一个非常实用的技巧只要节点不配置value字段该节点就不可被点击选中。例如在目录 / 文件模型中让目录节点只作为分组展示、仅文件节点可被选中{ type: form, api: /api/mock2/form/saveForm, body: [ { type: tree-select, name: tree, label: Tree, searchable: true, options: [ { label: Folder A, children: [ { label: file A, value: 2 }, { label: file B, value: 3 } ] }, { label: Folder E, children: [ { label: Folder G, children: [ { label: file H, value: 6 }, { label: file I, value: 7 } ] } ] } ] } ] }上例中Folder A、Folder E、Folder G都没有value因此只能展开、不能勾选而file A等带value的节点可以正常选中。这个行为在源码层面与resolveOptionTreeSelect.tsx呼应——查找选中项时按valueField默认value匹配无value的节点不参与值匹配因而无法被当作合法选中值。搜索选项TreeSelect 提供两种检索能力注意两者用途不同本地过滤配合searchable: true在静态options上做前端关键字过滤远程搜索配置autoComplete接口将term作为搜索关键字发给服务端搜索逻辑由服务端实现适合数据量大的场景。远程搜索示例2.7.1及以上版本{ 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 } ] }上例同时配置了source树初始数据源与autoComplete远程搜索接口。源码中 loadRemote 是远程搜索的核心实现请求通过 250ms 防抖触发TreeSelect.tsx避免每次输入都发请求请求参数为{...data, term: input, value: input}即页面数据与搜索词一并提交响应可返回{options: [...]}结构或直接返回选项数组(ret.data ret.data.options) || ret.data || []结果会按input关键字做本地缓存this.cache[input]相同关键词重复搜索直接命中缓存不再发请求同时配置source与autoComplete时首次渲染先加载source数据sourceLoaded标记控制后续输入再走远程搜索每次搜索结果会通过mergeOptions把已选中的节点合并回来TreeSelect.tsx保证已选项不会因搜索被清出列表。而本地过滤逻辑在 filterOptions使用matchSorter按labelField/valueField做模糊匹配CONTAINS阈值并递归过滤子节点只要某个子节点命中父节点就会保持展开collapsed false方便在过滤后快速看到命中的叶子。自定义选项渲染2.8.0 及以上版本使用menuTpl属性可以通过模板字符串自定义下拉菜单中每个选项的渲染内容适合展示字段名 类型标签等富信息。模板中可使用${label}、${value}以及 options 节点上的任意自定义字段如下例的tag、icon并可配合iconField指定图标字段{ type: form, api: /api/mock2/form/saveForm, body: [ { type: tree-select, name: tree, label: Tree, menuTpl: div classflex justify-betweenspan${label}/spanspan classbg-gray-200 rounded p-1 text-xs text-center w-24${tag}/span/div, iconField: icon, searchable: true, options: [ { label: 采购单, value: order, tag: 数据模型, icon: fa fa-database, children: [ { label: ID, value: id, tag: 数字, icon: fa fa-check }, { label: 采购人, value: name, tag: 字符串, icon: fa fa-check }, { label: 采购时间, value: time, tag: 日期时间, icon: fa fa-check }, { label: 供应商, value: vendor, tag: 数据模型(N:1), icon: fa fa-database, children: [ { label: 供应商ID, value: vendor_id, tag: 数字, icon: fa fa-check }, { label: 供应商名称, value: vendor_name, tag: 字符串, icon: fa fa-check } ] } ] } ] } ] }源码中menuTpl通过 renderOptionItem 生效当配置了menuTpl时组件把menuTpl当作一个 amis 模板渲染器渲染数据为data与option的合并对象createObject(createObject(data, {...states}), option)因此模板中可以自由引用选项节点上的任意字段实现字段名 数据类型标签 图标一体的选项卡片。属性表下列属性为tree-select独占属性更多通用属性如multiple、joinValues、extractValue、withChildren、cascade、autoCheckChildren、deferApi懒加载、showOutline展开线、creatable/editable/removable增删改等请参考 InputTree 树形选择框。属性名类型默认值说明版本hideNodePathLabelbooleanfalse是否隐藏选择框中已选择节点的路径 label 信息onlyLeafbooleanfalse只允许选择叶子节点searchablebooleanfalse是否可检索仅在 type 为tree-select的时候生效对searchable需要特别说明官方文档明确其仅在 type 为tree-select时生效即它是选择器形态浮层 可输入搜索框特有的能力。测试用例 Tree.test.tsx 还验证了一个细节单选模式下如果已有选中值即使开启searchable也不会渲染输入框singleModeInput不存在而多选模式始终保留输入框multipleModeInput存在这与源码中allowInput的判断逻辑multiple || !resultValue见 TreeSelect.tsx完全一致。此外TreeSelectControl还实现了基于minLength/maxLength的选中数量校验TreeSelect.tsx内部按delimiter默认,把值拆成数组后比较长度不满足时返回已选择数量低于设定的最小个数 / 超出设定的最大个数的校验错误。事件表TreeSelect 对外派发以下事件可通过onEvent监听并在actions中配置执行动作在actions中通过${事件参数名}或${event.data.[事件参数名]}获取事件产生的数据。事件动作的完整机制请查看事件动作。[name]表示当前组件绑定的名称即name属性如果没有配置name属性则通过value取值。事件名称事件参数说明change[name]: string组件的值item: object选中的节点6.2.0 及以上版本items: object[]选项集合3.6.0 及以上版本选中值变化时触发blur[name]: string组件的值item: object选中的节点6.2.0 及以上版本items: object[]选项集合3.6.4 及以上版本输入框失去焦点时触发focus[name]: string组件的值item: object选中的节点6.2.0 及以上版本items: object[]选项集合3.6.4 及以上版本输入框获取焦点时触发addConfirm (3.6.4 及以上版本)[name]: string组件的值item: object新增的节点信息items: object[]选项集合新增节点提交时触发editConfirm (3.6.4 及以上版本)[name]: object组件的值item: object编辑的节点信息items: object[]选项集合编辑节点提交时触发deleteConfirm (3.6.4 及以上版本)[name]: string组件的值item: object删除的节点信息items: object[]选项集合删除节点提交时触发deferLoadFinished (3.6.4 及以上版本)[name]: object组件的值result: objectdeferApi 懒加载远程请求成功后返回的数据items: object[]选项集合懒加载接口远程请求成功时触发itemClick (6.9.0 以上版本)item: Option所点击的选项节点点击时触发staticItemClick (6.13.0 以上版本)item: Option所点击的选项静态展示时节点点击时触发add不推荐[name]: object新增的节点信息items: object[]选项集合 2.3.2 及以下版本为options新增节点提交时触发edit不推荐[name]: object编辑的节点信息items: object[]选项集合 2.3.2 及以下版本为options编辑节点提交时触发delete不推荐[name]: object删除的节点信息items: object[]选项集合 2.3.2 及以下版本为options删除节点提交时触发loadFinished不推荐[name]: objectdeferApi 懒加载远程请求成功后返回的数据懒加载接口远程请求成功时触发从源码实现看resultChangeEvent 在值变化时通过dispatchEvent(change, ...)派发事件参数为{value, item, items}且事件是可拦截的——如果监听器返回rendererEvent.prevented true则onChange不会执行选中值不会真正提交。focus、blur事件则在 handleFocus 与 handleBlur 中派发同样携带{value, item, items}。itemClick由 handleNodeClick 派发携带所点击的item且同样支持prevented拦截。change{ type: form, api: /api/mock2/form/saveForm, debug: true, body: [ { type: tree-select, name: tree, label: Tree, onEvent: { change: { actions: [ { actionType: toast, args: { msg: ${event.data.tree|json} } } ] } }, options: [ { label: Folder A, value: 1, children: [ { label: file A, value: 2 }, { label: file B, value: 3 } ] }, { label: file C, value: 4 }, { label: file D, value: 5 } ] } ] }focus{ type: form, api: /api/mock2/form/saveForm, debug: true, body: [ { type: tree-select, name: tree, label: Tree, onEvent: { focus: { actions: [ { actionType: toast, args: { msg: ${event.data.tree|json} } } ] } }, options: [ { label: Folder A, value: 1, children: [ { label: file A, value: 2 }, { label: file B, value: 3 } ] }, { label: file C, value: 4 }, { label: file D, value: 5 } ] } ] }blur{ type: form, api: /api/mock2/form/saveForm, debug: true, body: [ { type: tree-select, name: tree, label: Tree, onEvent: { blur: { actions: [ { actionType: toast, args: { msg: ${event.data.tree|json} } } ] } }, options: [ { label: Folder A, value: 1, children: [ { label: file A, value: 2 }, { label: file B, value: 3 } ] }, { label: file C, value: 4 }, { label: file D, value: 5 } ] } ] }addConfirm配置creatable: true后可监听确认新增操作{ type: form, api: /api/mock2/form/saveForm, debug: true, body: [ { type: tree-select, name: tree, label: Tree, creatable: true, removable: true, editable: true, onEvent: { addConfirm: { actions: [ { actionType: toast, args: { msg: ${event.data.item|json} } } ] } }, options: [ { label: Folder A, value: 1, children: [ { label: file A, value: 2 }, { label: file B, value: 3 } ] }, { label: file C, value: 4 }, { label: file D, value: 5 } ] } ] }editConfirm配置editable: true后可监听确认编辑操作{ type: form, api: /api/mock2/form/saveForm, debug: true, body: [ { type: tree-select, name: tree, label: Tree, creatable: true, removable: true, editable: true, onEvent: { editConfirm: { actions: [ { actionType: toast, args: { msg: ${event.data.item|json} } } ] } }, options: [ { label: Folder A, value: 1, children: [ { label: file A, value: 2 }, { label: file B, value: 3 } ] }, { label: file C, value: 4 }, { label: file D, value: 5 } ] } ] }deleteConfirm配置removable: true后可监听确认删除操作{ type: form, api: /api/mock2/form/saveForm, debug: true, body: [ { type: tree-select, name: tree, label: Tree, creatable: true, removable: true, editable: true, onEvent: { deleteConfirm: { actions: [ { actionType: toast, args: { msg: ${event.data.item|json} } } ] } }, options: [ { label: Folder A, value: 1, children: [ { label: file A, value: 2 }, { label: file B, value: 3 } ] }, { label: file C, value: 4 }, { label: file D, value: 5 } ] } ] }deferLoadFinished懒加载deferdeferApi请求成功后触发。配置了defer: true的节点会在展开时按deferApi拉取子节点event.data.result为接口返回数据{ type: form, api: /api/mock2/form/saveForm, debug: true, body: [ { type: tree-select, name: tree, label: Tree, deferApi: /api/mock2/form/deferOptions?label${label}waitSeconds2, onEvent: { deferLoadFinished: { actions: [ { actionType: toast, args: { msg: ${event.data.result|json} } } ] } }, options: [ { label: Folder A, value: 1, collapsed: true, children: [ { label: file A, value: 2 }, { label: file B, value: 3 } ] }, { label: 这下面是懒加载的, value: 4, defer: true }, { label: file D, value: 5 } ] } ] }itemClick6.9.0 以上版本节点被点击时触发事件参数为所点击的选项item{ type: form, api: /api/mock2/form/saveForm, debug: true, body: [ { type: input-tree, name: tree, label: Tree, nodeBehavior: [], onEvent: { itemClick: { actions: [ { actionType: toast, args: { msg: ${event.data.item|json} } } ] } }, options: [ { label: Folder A, value: 1, children: [ { label: file A, value: 2 }, { label: file B, value: 3 } ] }, { label: file C, value: 4 }, { label: file D, value: 5 } ] } ] }staticItemClick6.13.0 以上版本静态展示static: true场景下点击节点标签时触发{ type: form, api: /api/mock2/form/saveForm, debug: true, body: [ { type: input-tree, name: tree, label: Tree, nodeBehavior: [], multiple: true, value: 4,5, inTag: true, static: true, onEvent: { staticItemClick: { actions: [ { actionType: toast, args: { msg: ${event.data.item|json} } } ] } }, options: [ { label: Folder A, value: 1, children: [ { label: file A, value: 2 }, { label: file B, value: 3 } ] }, { label: file C, value: 4 }, { label: file D, value: 5 } ] } ] }动作表TreeSelect 对外暴露以下特性动作其他组件可通过actionType: 动作名称componentId: 该组件id触发动作参数通过args: {动作配置项名称: xxx}配置。机制详见事件动作。动作名称动作配置说明additem: Option, parentValue?: anyitem 新增的数据项parentValue 父级数据项的 value如果配置了 valueField以 valueField 的字段值为准edititem: Option, originValue: anyitem 编辑后的数据项originValue 编辑前数据项的 value如果配置了 valueField以 valueField 的字段值为准deletevalue: any删除数据项的 value如果配置了 valueField以 valueField 的字段值为准reload-刷新clear-清空reset-将值重置为初始值。6.3.0 及以下版本为resetValuesetValuevalue: string|string[]更新的值更新数据开启multiple支持设置多项开启joinValues时多值用,分隔否则多值用数组动作分发的入口是 doActionclear调用clearValue()reset调用resetValue()add/edit/delete分别调用addItemFromAction/editItemFromAction/deleteItemFromActionreload调用reload()。其中add通过findTreeIndex按parentValue定位父节点索引后调用onAddTreeSelect.tsxedit先按originValue解析出原节点再以{...item, originValue}作为编辑结果调用onEditTreeSelect.tsxdelete按value解析出节点后调用onDeleteTreeSelect.tsx。clear{ type: form, debug: true, body: [ { type: tree-select, name: tree, label: Tree, options: [ { label: Folder A, value: 1, children: [ { label: file A, value: 2 }, { label: Folder B, value: 3, children: [ { label: file b1, value: 3.1 }, { label: file b2, value: 3.2 } ] } ] }, { label: file C, value: 4 }, { label: file D, value: 5 } ], value: 5, id: clear_text }, { type: button, label: 清空, onEvent: { click: { actions: [ { actionType: clear, componentId: clear_text } ] } } } ] }reset如果配置了resetValue重置时使用resetValue的值否则使用表单初始值{ type: form, debug: true, body: [ { type: tree-select, name: tree, label: Tree, options: [ { label: Folder A, value: 1, children: [ { label: file A, value: 2 }, { label: Folder B, value: 3, children: [ { label: file b1, value: 3.1 }, { label: file b2, value: 3.2 } ] } ] }, { label: file C, value: 4 }, { label: file D, value: 5 } ], value: 5, id: reset_text }, { type: button, label: 重置, onEvent: { click: { actions: [ { actionType: reset, componentId: reset_text } ] } } } ] }关于clear与reset的区别可从源码看得很清楚clearValue 将值设置为resetValue未配置时为而 resetValue 优先取表单的原始值pristinegetVariable(formStore?.pristine ?? store?.pristine, name)取不到时才回退到resetValue。也就是说reset是回到初始状态clear是直接清空。补充说明与最佳实践单选与多选multiple: false是默认单选开启多选后配合joinValues默认true值以delimiter默认,分隔的字符串提交extractValue可进一步只提交valueField值。删除选中项时按住Backspace可快速移除最后一个handleInputKeyDown。父子联动withChildren、onlyChildren、cascade、autoCheckChildren、autoCancelParent等控制父子节点的勾选联动关系属 InputTree 树形选择框 的通用能力TreeSelect 均继承。懒加载节点配置defer: true配合deferApi可在展开节点时按需请求子节点配合deferLoadFinished事件处理加载完成后的业务逻辑loadFinished为旧版不推荐事件名。编辑器集成在 amis 可视化编辑器中TreeSelect 由 TreeSelect.tsx 插件注册支持rendererName tree-select自带树型结构选择支持内嵌模式与浮层模式外观切换的描述、搜索关键词与默认脚手架包含选项A/选项C/选项D的示例树可在编辑器里拖拽配置。组件选型tree-select与input-tree本质共享同一套树组件差异在于外观形态——tree-select是输入框 浮层的选择器样式input-tree是直接平铺的内嵌树文档中itemClick、staticItemClick示例也同时适用于input-tree形态。【免费下载链接】amis前端低代码框架通过 JSON 配置就能生成各种页面。项目地址: https://gitcode.com/GitHub_Trending/am/amis创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考