这个是在若依框架无意中发现的一个下拉树通用组件。@riophae/vue-treeselect 是一个基于 Vue.js 的树形选择器组件,可以用于选择树形结构的数据。它支持多选、搜索、异步加载等功能,可以自定义选项的样式和模板。该组件易于使用和扩展,适用于各种类型的项目。

npm:https://www.npmjs.com/package/@riophae/vue-treeselect

首先安装:

使用自己习惯使用的包管理器安装就可以了

pnpm add @riophae/vue-treeselect

引入注册:

import Treeselect from '@riophae/vue-treeselect'import '@riophae/vue-treeselect/dist/vue-treeselect.css'export default {components: { Treeselect } }

基本使用:

// import the componentimport Treeselect from '@riophae/vue-treeselect'// import the stylesimport '@riophae/vue-treeselect/dist/vue-treeselect.css'export default {// register the componentcomponents: { Treeselect },data() {return {// define the default valuevalue: null,// define optionsoptions: [ {id: 'a',label: 'a',children: [ {id: 'aa',label: 'aa',}, {id: 'ab',label: 'ab',} ],}, {id: 'b',label: 'b',}, {id: 'c',label: 'c',} ],}},}

里面可配置的属性很多,下面是在源码中看到的:

部分注释百度翻译成中文了,但太多了,懒得挨个翻译了,直接看也大概知道啥意思

props: {/** * 即使有禁用的选定节点,是否允许重置值 */allowClearingDisabled: {type: Boolean,default: false,},/** * 选择/取消选择祖先节点时,是否应选择/取消选中其禁用的后代 * 和 allowClearingDisabled 一起使用 */allowSelectingDisabledDescendants: {type: Boolean,default: false,},/** * 菜单是否应始终打开 */alwaysOpen: {type: Boolean,default: false,},/** * 是否将菜单加到body上 */appendToBody: {type: Boolean,default: false,},/** * 是否启用异步搜索模式 */async: {type: Boolean,default: false,},/** * 是否自动将组件集中在装载上? */autoFocus: {type: Boolean,default: false,},/** * 装载时自动加载根选项。当设置为“false”时,打开菜单时将加载根选项。 */autoLoadRootOptions: {type: Boolean,default: true,},/** * 当用户取消选择一个节点时,会自动取消选择其祖先。仅适用于平面模式。 */autoDeselectAncestors: {type: Boolean,default: false,},/** * 当用户取消选择节点时,会自动取消选择其子节点。仅适用于平面模式。 */autoDeselectDescendants: {type: Boolean,default: false,},/** * 当用户选择一个节点时,会自动选择其祖先。仅适用于平面模式。 */autoSelectAncestors: {type: Boolean,default: false,},/** * 当用户选择一个节点时,会自动选择其子节点。仅适用于平面模式。 */autoSelectDescendants: {type: Boolean,default: false,},/** * 如果没有文本输入,按退格键是否删除最后一项。 */backspaceRemoves: {type: Boolean,default: true,},/** * 在清除所有输入字段之前进行处理的函数。 * 返回“false”以防止清除值 * @type {function(): (boolean|Promise)} */beforeClearAll: {type: Function,default: constant(true),},/** * 在叶节点之前显示分支节点? */branchNodesFirst: {type: Boolean,default: false,},/** * 是否应该缓存每个搜索请求的结果? */cacheOptions: {type: Boolean,default: true,},/** * 是否显示重置值的“×”按钮? */clearable: {type: Boolean,default: true,},/** * 清楚文本,multiple为true时 */clearAllText: {type: String,default: 'Clear all',},/** * 选择后是否清除搜索输入。 * 仅当“multiple”为“true”时使用。 * 对于单选模式,无论道具值如何,它总是**在选择一个选项后清除输入。 */clearOnSelect: {type: Boolean,default: false,},/** * “×”按钮的标题。 */clearValueText: {type: String,default: 'Clear value',},/** * 选择选项后是否关闭菜单? * 仅当“multiple”为“true”时使用。 */closeOnSelect: {type: Boolean,default: true,},/** * 加载时应自动展开多少级别的分支节点。 * 设置Infinity以使所有分支节点在默认情况下展开。 */defaultExpandLevel: {type: Number,default: 0,},/** * 在用户开始搜索之前要显示的默认选项集。用于异步搜索模式。 * 当设置为“true”时,将自动加载作为空字符串的搜索查询结果。 * @type {boolean|node[]} */defaultOptions: {default: false,},/** * 如果没有文本输入,按delete键是否删除最后一项。 */deleteRemoves: {type: Boolean,default: true,},/** * 用于连接隐藏字段值的多个值的分隔符。 */delimiter: {type: String,default: ',',},/** * 仅显示与搜索值直接匹配的节点,不包括其祖先。 * * @type {Object} */flattenSearchResults: {type: Boolean,default: false,},/** * 是否阻止选择分支节点? */disableBranchNodes: {type: Boolean,default: false,},/** * 禁用控制? */disabled: {type: Boolean,default: false,},/** * 是否禁用模糊匹配功能? */disableFuzzyMatching: {type: Boolean,default: false,},/** *是否启用平面模式。非平面模式(默认)是指:* - 每当检查分支节点时,它的所有子节点也将被检查 * - 每当一个分支节点检查了所有子节点时,该分支节点本身也会被检查 * 设置“true”以禁用此机制 */flat: {type: Boolean,default: false,},/** * 将以所有事件作为最后一个参数进行传递。 * 有助于识别事件的起源。*/instanceId: {// Add two trailing "$" to distinguish from explictly specified ids.default: () => `${instanceId++}$$`,type: [String, Number],},/** * Joins multiple values into a single form field with the `delimiter` (legacy mode). * 使用“分隔符”将多个值合并到一个表单字段中(传统模式)。*/joinValues: {type: Boolean,default: false,},/** * 限制所选选项的显示。 * 其余部分将隐藏在limitText字符串中。 */limit: {type: Number,default: Infinity,},/** * Function that processes the message shown when selected elements pass the defined limit. * @type {function(number): string} */limitText: {type: Function,default: function limitTextDefault(count) { // eslint-disable-line func-name-matchingreturn `and ${count} more`},},/** * Text displayed when loading options. */loadingText: {type: String,default: 'Loading...',},/** * Used for dynamically loading options. * @type {function({action: string, callback: (function((Error|string)=): void), parentNode: node=, instanceId}): void} */loadOptions: {type: Function,},/** * Which node properties to filter on. */matchKeys: {type: Array,default: constant(['label']),},/** * Sets `maxHeight` style value of the menu. */maxHeight: {type: Number,default: 300,},/** * Set `true` to allow selecting multiple options (a.k.a., multi-select mode). */multiple: {type: Boolean,default: false,},/** * Generates a hidden  tag with this field name for html forms. */name: {type: String,},/** * Text displayed when a branch node has no children. */noChildrenText: {type: String,default: 'No sub-options.',},/** * Text displayed when there are no available options. */noOptionsText: {type: String,default: 'No options available.',},/** * Text displayed when there are no matching search results. */noResultsText: {type: String,default: 'No results found...',},/** * Used for normalizing source data. * @type {function(node, instanceId): node} */normalizer: {type: Function,default: identity,},/** * By default (`auto`), the menu will open below the control. If there is not * enough space, vue-treeselect will automatically flip the menu. * You can use one of other four options to force the menu to be always opened * to specified direction. * Acceptable values: * - `"auto"` * - `"below"` * - `"bottom"` * - `"above"` * - `"top"` */openDirection: {type: String,default: 'auto',validator(value) {const acceptableValues = ['auto', 'top', 'bottom', 'above', 'below']return includes(acceptableValues, value)},},/** * Whether to automatically open the menu when the control is clicked. */openOnClick: {type: Boolean,default: true,},/** * Whether to automatically open the menu when the control is focused. */openOnFocus: {type: Boolean,default: false,},/** * Array of available options. * @type {node[]} */options: {type: Array,},/** * Field placeholder, displayed when there's no value. */placeholder: {type: String,default: 'Select...',},/** * Applies HTML5 required attribute when needed. */required: {type: Boolean,default: false,},/** * Text displayed asking user whether to retry loading children options. */retryText: {type: String,default: 'Retry?',},/** * Title for the retry button. */retryTitle: {type: String,default: 'Click to retry',},/** * Enable searching feature? */searchable: {type: Boolean,default: true,},/** * Search in ancestor nodes too. */searchNested: {type: Boolean,default: false,},/** * Text tip to prompt for async search. */searchPromptText: {type: String,default: 'Type to search...',},/** * Whether to show a children count next to the label of each branch node. */showCount: {type: Boolean,default: false,},/** * Used in conjunction with `showCount` to specify which type of count number should be displayed. * Acceptable values: * - "ALL_CHILDREN" * - "ALL_DESCENDANTS" * - "LEAF_CHILDREN" * - "LEAF_DESCENDANTS" */showCountOf: {type: String,default: ALL_CHILDREN,validator(value) {const acceptableValues = [ALL_CHILDREN, ALL_DESCENDANTS, LEAF_CHILDREN, LEAF_DESCENDANTS]return includes(acceptableValues, value)},},/** * Whether to show children count when searching. * Fallbacks to the value of `showCount` when not specified. * @type {boolean} */showCountOnSearch: null,/** * In which order the selected options should be displayed in trigger & sorted in `value` array. * Used for multi-select mode only. * Acceptable values: * - "ORDER_SELECTED" * - "LEVEL" * - "INDEX" */sortValueBy: {type: String,default: ORDER_SELECTED,validator(value) {const acceptableValues = [ORDER_SELECTED, LEVEL, INDEX]return includes(acceptableValues, value)},},/** * Tab index of the control. */tabIndex: {type: Number,default: 0,},/** * The value of the control. * Should be `id` or `node` object for single-select mode, or an array of `id` or `node` object for multi-select mode. * Its format depends on the `valueFormat` prop. * For most cases, just use `v-model` instead. * @type {?Array} */value: null,/** * Which kind of nodes should be included in the `value` array in multi-select mode. * Acceptable values: * - "ALL" - Any node that is checked will be included in the `value` array * - "BRANCH_PRIORITY" (default) - If a branch node is checked, all its descendants will be excluded in the `value` array * - "LEAF_PRIORITY" - If a branch node is checked, this node itself and its branch descendants will be excluded from the `value` array but its leaf descendants will be included * - "ALL_WITH_INDETERMINATE" - Any node that is checked will be included in the `value` array, plus indeterminate nodes */valueConsistsOf: {type: String,default: BRANCH_PRIORITY,validator(value) {const acceptableValues = [ALL, BRANCH_PRIORITY, LEAF_PRIORITY, ALL_WITH_INDETERMINATE]return includes(acceptableValues, value)},},/** * Format of `value` prop. * Note that, when set to `"object"`, only `id` & `label` properties are required in each `node` object in `value` prop. * Acceptable values: * - "id" * - "object" */valueFormat: {type: String,default: 'id',},/** * z-index of the menu. */zIndex: {type: [Number, String],default: 999,}}

然后我简单看了一下好像一共向外暴露了6个方法如下:

@input // // 选中触发(第一次回显的时候会触发,清除值的时候会触发, value值为undefined) input事件用于v-model双向绑定组件更新父组件值@select // 选中触发(清除值的时候不会触发)@deselect // 移除选项时触发 当设置multiple为true时生效raw为当前移除的对象@search-change // 搜索触发(输入框输入 值改变时)@open // 展开时触发@close // 关闭时触发

下面是我测试的一个例子,一般的需求应该足够了

字体样式简单调了一下

import Treeselect from '@riophae/vue-treeselect'import '@riophae/vue-treeselect/dist/vue-treeselect.css'import treeData from './data/tree'export default {data() {return {value: null,options: []}},components: { Treeselect },mounted(){// 延迟模拟请求数据setTimeout(() => {this.options = treeDatathis.value = [111, 113] // 测试回显操作}, 300)},methods:{// 选中触发(第一次回显的时候会触发,清除值的时候会触发, value值为undefined) input事件用于v-model双向绑定组件更新父组件值treeSelectInput(value, instanceId) {console.log(value, 'input事件')console.log(this.value, 'this.value -- input') // 这个不需要 延迟},// 选中触发(清除值的时候不会触发)treeSelectChange(raw, instanceId) {console.log(raw, '当前的对象')setTimeout(() => { // 如果用到this.value 需要setTimeout延迟一下拿到最新的值console.log(this.value, 'this.value -- select')})},// 移除选项时触发 当设置multiple为true时生效raw为当前移除的对象treeSelectDeselect(raw, instanceId) {console.log(raw, 'deselect-->>')},// 搜索treeSelectSearch(searchQuery, instanceId) {console.log(searchQuery, '当前搜索的值')},// 展开触发treeSelectOpen(instanceId) {console.log('展开了')},// 关闭触发treeSelectClose(value, instanceId) {console.log(value, '当前的value值')},// 字段默认 id label 用于规范化数据源tenantIdnormalizer(node, instanceId) {if (node.children && !node.children.length) {delete node.children}return {id: node.id,label: node.title,children: node.children}}}}.main {width: 100%;height: 100%;padding: 60px 0 0 200px;}.main .tree {width: 240px;height: 40px;}::v-deep .vue-treeselect__label {color: #606266;}

测试数据:

export default [{"title": "系统管理","parentId": 0,"id": 1,"children": [{"title": "菜单管理","parentId": 1,"id": 11,"children": [{"title": "菜单新增","parentId": 11,"id": 111},{"title": "菜单编辑","parentId": 11,"id": 112},{"title": "菜单删除","parentId": 11,"id": 113}]},{"title": "角色管理","parentId": 1,"id": 22,"children": [{"title": "角色编辑","parentId": 22,"id": 222},{"title": "角色新增","parentId": 22,"id": 221},{"title": "角色删除","parentId": 22,"id": 223}]}]},{"title": "用户管理","parentId": 0,"id": 33,"children": [{"title": "用户新增","parentId": 33,"id": 331},{"title": "用户编辑","parentId": 33,"id": 332},{"title": "用户删除","parentId": 33,"id": 333}]}]

效果如下: