这个是在若依框架无意中发现的一个下拉树通用组件。@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 }
}
基本使用:
<template>
<div id="app">
<treeselect v-model="value" :multiple="true" :options="options" />
</div>
</template>
<script>
// import the component
import Treeselect from '@riophae/vue-treeselect'
// import the styles
import '@riophae/vue-treeselect/dist/vue-treeselect.css'
export default {
// register the component
components: { Treeselect },
data() {
return {
// define the default value
value: null,
// define options
options: [ {
id: 'a',
label: 'a',
children: [ {
id: 'aa',
label: 'aa',
}, {
id: 'ab',
label: 'ab',
} ],
}, {
id: 'b',
label: 'b',
}, {
id: 'c',
label: 'c',
} ],
}
},
}
</script>
里面可配置的属性很多,下面是在源码中看到的:
部分注释百度翻译成中文了,但太多了,懒得挨个翻译了,直接看也大概知道啥意思
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<boolean>)}
*/
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-matching
return `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 <input /> 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 // 关闭时触发
下面是我测试的一个例子,一般的需求应该足够了
字体样式简单调了一下
<template>
<div class="main">
<div class="tree">
<Treeselect
v-model="value"
:options="options"
:placeholder="'请选择人员'"
:normalizer="tenantIdnormalizer"
:multiple="true"
@input="treeSelectInput"
@select="treeSelectChange"
@deselect="treeSelectDeselect"
@search-change="treeSelectSearch"
@open="treeSelectOpen"
@close="treeSelectClose"
/>
</div>
</div>
</template>
<script>
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 = treeData
this.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
}
}
}
}
</script>
<style scoped>
.main {
width: 100%;
height: 100%;
padding: 60px 0 0 200px;
}
.main .tree {
width: 240px;
height: 40px;
}
::v-deep .vue-treeselect__label {
color: #606266;
}
</style>
测试数据:
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
}
]
}
]
效果如下: